Context aware escaping

This commit is contained in:
Sergey
2017-02-22 11:47:25 -08:00
committed by Juan Martín Sotuyo Dodero
parent 18d9c12467
commit 93f82fbd20
2 changed files with 206 additions and 68 deletions

View File

@ -4,6 +4,7 @@
package net.sourceforge.pmd.lang.vf.rule.security;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
@ -20,15 +21,11 @@ import net.sourceforge.pmd.lang.vf.rule.AbstractVfRule;
*
*/
public class VfUnescapeElRule extends AbstractVfRule {
private static final String JSINHTMLENCODE = "JSINHTMLENCODE";
private static final String URLENCODE = "URLENCODE";
private static final String HTMLENCODE = "HTMLENCODE";
private static final String APEX_PARAM = "apex:param";
private static final String VALUE = "value";
private static final String ITEM_VALUE = "itemValue";
private static final String ITEM_VALUE = "itemvalue";
private static final String ESCAPE = "escape";
private static final String ITEM_ESCAPED = "itemEscaped";
private static final String ITEM_ESCAPED = "itemescaped";
private static final String APEX_OUTPUT_TEXT = "apex:outputtext";
private static final String APEX_PAGE_MESSAGE = "apex:pagemessage";
private static final String APEX_PAGE_MESSAGES = "apex:pagemessages";
@ -38,62 +35,106 @@ public class VfUnescapeElRule extends AbstractVfRule {
@Override
public Object visit(ASTElement node, Object data) {
if (doesTagSupportEscaping(node)) {
final List<ASTAttribute> attributes = node.findChildrenOfType(ASTAttribute.class);
boolean isUnescaped = false;
boolean isEL = false;
boolean hasPlaceholders = false;
for (ASTAttribute attr : attributes) {
if (attr != null) {
String name = attr.getName();
switch (name) {
case ESCAPE:
case ITEM_ESCAPED:
final ASTText text = attr.getFirstDescendantOfType(ASTText.class);
if (text != null) {
if (text.getImage().equalsIgnoreCase(FALSE)) {
isUnescaped = true;
}
}
break;
case VALUE:
case ITEM_VALUE:
final ASTElExpression elInVal = attr.getFirstDescendantOfType(ASTElExpression.class);
if (doesElContainAnyUnescapedIdentifiers(elInVal)) {
isEL = true;
}
final ASTText textValue = attr.getFirstDescendantOfType(ASTText.class);
if (textValue != null) {
if (Pattern.compile("\\{(\\w|,|\\.|'|:|\\s)*\\}").matcher(textValue.getImage()).matches()) {
hasPlaceholders = true;
}
}
break;
default:
break;
}
}
}
if (hasPlaceholders && isUnescaped) {
if (hasELInInnerElements(node)) {
addViolation(data, node);
}
}
if (isEL && isUnescaped) {
addViolation(data, node);
}
checkTagsThatSupportEscaping(node, data);
} else {
checkAllOtherTags(node, data);
}
return super.visit(node, data);
}
private boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression) {
private void checkAllOtherTags(ASTElement node, Object data) {
final List<ASTAttribute> attributes = node.findChildrenOfType(ASTAttribute.class);
boolean isEL = false;
for (ASTAttribute attr : attributes) {
String name = attr.getName().toLowerCase();
// look for onevents
if (Pattern.compile("^on(\\w)+$").matcher(name).matches()) {
final List<ASTElExpression> elsInVal = attr.findDescendantsOfType(ASTElExpression.class);
for (ASTElExpression el : elsInVal) {
if (doesElContainAnyUnescapedIdentifiers(el,
Arrays.asList(ESCAPING.JSINHTMLENCODE, ESCAPING.JSENCODE))) {
isEL = true;
}
}
}
if (name.equalsIgnoreCase("href") || name.equalsIgnoreCase("src")) {
final List<ASTElExpression> elsInVal = attr.findDescendantsOfType(ASTElExpression.class);
for (ASTElExpression el : elsInVal) {
if (doesElContainAnyUnescapedIdentifiers(el, ESCAPING.URLENCODE)) {
isEL = true;
}
}
}
}
if (isEL) {
addViolation(data, node);
}
}
private void checkTagsThatSupportEscaping(ASTElement node, Object data) {
final List<ASTAttribute> attributes = node.findChildrenOfType(ASTAttribute.class);
boolean isUnescaped = false;
boolean isEL = false;
boolean hasPlaceholders = false;
for (ASTAttribute attr : attributes) {
String name = attr.getName().toLowerCase();
switch (name) {
case ESCAPE:
case ITEM_ESCAPED:
final ASTText text = attr.getFirstDescendantOfType(ASTText.class);
if (text != null) {
if (text.getImage().equalsIgnoreCase(FALSE)) {
isUnescaped = true;
}
}
break;
case VALUE:
case ITEM_VALUE:
final List<ASTElExpression> elsInVal = attr.findDescendantsOfType(ASTElExpression.class);
for (ASTElExpression el : elsInVal) {
if (doesElContainAnyUnescapedIdentifiers(el, ESCAPING.HTMLENCODE)) {
isEL = true;
}
}
final ASTText textValue = attr.getFirstDescendantOfType(ASTText.class);
if (textValue != null) {
if (Pattern.compile("\\{(\\w|,|\\.|'|:|\\s)*\\}").matcher(textValue.getImage()).matches()) {
hasPlaceholders = true;
}
}
break;
default:
break;
}
}
if (hasPlaceholders && isUnescaped) {
if (hasELInInnerElements(node)) {
addViolation(data, node);
}
}
if (isEL && isUnescaped) {
addViolation(data, node);
}
}
private boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, ESCAPING escape) {
return doesElContainAnyUnescapedIdentifiers(elExpression, Arrays.asList(escape));
}
private boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, List<ESCAPING> escapes) {
if (elExpression == null) {
return false;
}
@ -104,15 +145,23 @@ public class VfUnescapeElRule extends AbstractVfRule {
for (final ASTIdentifier id : ids) {
if (id.getImage() != null) {
switch (id.getImage()) {
case HTMLENCODE:
case URLENCODE:
case JSINHTMLENCODE:
isEscaped = true;
break;
default:
break;
for (ESCAPING e : escapes) {
if (id.getImage().equalsIgnoreCase(e.toString())) {
isEscaped = true;
break;
}
if (e.equals(ESCAPING.ANY)) {
for (ESCAPING esc : ESCAPING.values()) {
if (id.getImage().equalsIgnoreCase(esc.toString())) {
isEscaped = true;
break;
}
}
}
}
}
}
@ -124,7 +173,7 @@ public class VfUnescapeElRule extends AbstractVfRule {
return false;
}
switch (node.getName().toLowerCase()) { // vf is case insensitive?
switch (node.getName().toLowerCase()) { // vf is case insensitive
case APEX_OUTPUT_TEXT:
case APEX_PAGE_MESSAGE:
case APEX_PAGE_MESSAGES:
@ -146,7 +195,7 @@ public class VfUnescapeElRule extends AbstractVfRule {
for (ASTAttribute attrib : innerAttributes) {
final List<ASTElExpression> elsInVal = attrib.findDescendantsOfType(ASTElExpression.class);
for (final ASTElExpression el : elsInVal) {
if (doesElContainAnyUnescapedIdentifiers(el)) {
if (doesElContainAnyUnescapedIdentifiers(el, ESCAPING.ANY)) {
return true;
}
@ -159,4 +208,23 @@ public class VfUnescapeElRule extends AbstractVfRule {
return false;
}
enum ESCAPING {
HTMLENCODE("HTMLENCODE"),
URLENCODE("URLENCODE"),
JSINHTMLENCODE("JSINHTMLENCODE"),
JSENCODE("JSENCODE"),
ANY("ANY");
private final String text;
private ESCAPING(final String text) {
this.text = text;
}
@Override
public String toString() {
return text;
}
}
}

View File

@ -1,5 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<test-data>
<test-code>
<description><![CDATA[
EL in JS on-event handler - stored XSS
]]></description>
<expected-problems>1</expected-problems>
<code><![CDATA[
<apex:page>
<div onclick="{!XSSHere(bah)}" />
</apex:page>
]]></code>
<source-type>vf</source-type>
</test-code>
<test-code>
<description><![CDATA[
EL in JS on-event handler - stored XSS
]]></description>
<expected-problems>0</expected-problems>
<code><![CDATA[
<apex:page>
<div onclick="{!JSENCODE(NoXSSHere)}" />
<div onclick="{!JSINHTMLENCODE(NoXSSHere)}" />
<div onclick="{!JSENCODE(NoXSSHere)}" />
</apex:page>
]]></code>
<source-type>vf</source-type>
</test-code>
<test-code>
<description><![CDATA[
EL in JS src handler - stored XSS
]]></description>
<expected-problems>1</expected-problems>
<code><![CDATA[
<apex:page>
<iframe src="{!XSSHere}" />
</apex:page>
]]></code>
<source-type>vf</source-type>
</test-code>
<test-code>
<description><![CDATA[
EL in JS src handler - stored XSS
]]></description>
<expected-problems>0</expected-problems>
<code><![CDATA[
<apex:page>
<iframe src="{!URLENCODE(XSSHere)}" />
</apex:page>
]]></code>
<source-type>vf</source-type>
</test-code>
<test-code>
<description><![CDATA[
EL in JS src handler - stored XSS
]]></description>
<expected-problems>1</expected-problems>
<code><![CDATA[
<apex:page>
<iframe src="{!XSSHere}" />
</apex:page>
]]></code>
<source-type>vf</source-type>
</test-code>
<test-code>
<description><![CDATA[
Default escaped EL - no XSS
@ -15,7 +85,7 @@ Default escaped EL - no XSS
<test-code>
<description><![CDATA[
No XSS
EL that is properly escaped should be no XSS
]]></description>
<expected-problems>0</expected-problems>
<code><![CDATA[