1
0
forked from phoedos/pmd
- ElEscapeDetector is utility class now
- Improved description and example of new rule
This commit is contained in:
Andreas Dangel
2021-01-21 15:01:58 +01:00
parent 15dd8783c7
commit dc4cdf696e
4 changed files with 66 additions and 51 deletions
pmd-visualforce/src/main
java/net/sourceforge/pmd/lang/vf/rule/security
resources/category/vf

@ -1,4 +1,4 @@
/**
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
@ -23,8 +23,6 @@ public class VfHtmlStyleTagXssRule extends AbstractVfRule {
private static final EnumSet<ElEscapeDetector.Escaping> ANY_ENCODE = EnumSet.of(ElEscapeDetector.Escaping.ANY);
private static final Pattern URL_METHOD_PATTERN = Pattern.compile("url\\s*\\([^)]*$", Pattern.CASE_INSENSITIVE);
private final ElEscapeDetector escapeDetector = new ElEscapeDetector();
public VfHtmlStyleTagXssRule() {
addRuleChainVisit(ASTElExpression.class);
}
@ -34,12 +32,13 @@ public class VfHtmlStyleTagXssRule extends AbstractVfRule {
* placed inside an ASTContent, which in turn is placed inside
* an ASTElement, where the element is not an inbuilt vf tag.
*
* <pre>{@code
* <ASTElement>
* <ASTContent>
* <ASTElExpression></ASTElExpression>
* </ASTContent>
* </ASTElement>
*
* }</pre>
*/
@Override
public Object visit(ASTElExpression node, Object data) {
@ -81,7 +80,7 @@ public class VfHtmlStyleTagXssRule extends AbstractVfRule {
ASTElement elementNode,
Object data) {
final String previousText = getPreviousText(contentNode, node);
final boolean isWithinSafeResource = escapeDetector.startsWithSafeResource(node);
final boolean isWithinSafeResource = ElEscapeDetector.startsWithSafeResource(node);
// if El is inside a <style></style> tag
// and is not surrounded by a safe resource, check for violations
@ -104,7 +103,7 @@ public class VfHtmlStyleTagXssRule extends AbstractVfRule {
private void verifyEncodingWithinUrl(ASTElExpression elExpressionNode, Object data) {
// only allow URLENCODING or JSINHTMLENCODING
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(
elExpressionNode,
URLENCODE_JSINHTMLENCODE)) {
addViolationWithMessage(
@ -116,7 +115,7 @@ public class VfHtmlStyleTagXssRule extends AbstractVfRule {
}
private void verifyEncodingWithoutUrl(ASTElExpression elExpressionNode, Object data) {
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(
elExpressionNode,
ANY_ENCODE)) {
addViolationWithMessage(
@ -132,15 +131,18 @@ public class VfHtmlStyleTagXssRule extends AbstractVfRule {
}
/**
* Get text content within style tag that leads upto the ElExpression.
* Get text content within style tag that leads up to the ElExpression.
* For example, in this snippet:
* <style>
*
* <pre>
* &lt;style>
* div {
* background: url('{!HTMLENCODE(XSSHere)}');
* }
* </style>
* &lt;/style>
* </pre>
*
* getPreviousText(...) would return "\n div {\n background: url("
* {@code getPreviousText(...)} would return <code>"\n div {\n background: url("</code>.
*
*/
private String getPreviousText(ASTContent content, ASTElExpression elExpressionNode) {

@ -49,8 +49,6 @@ public class VfUnescapeElRule extends AbstractVfRule {
private static final EnumSet<ElEscapeDetector.Escaping> JSENCODE_JSINHTMLENCODE = EnumSet.of(ElEscapeDetector.Escaping.JSENCODE, ElEscapeDetector.Escaping.JSINHTMLENCODE);
private static final EnumSet<ElEscapeDetector.Escaping> ANY_ENCODE = EnumSet.of(ElEscapeDetector.Escaping.ANY);
private final ElEscapeDetector escapeDetector = new ElEscapeDetector();
@Override
public Object visit(ASTHtmlScript node, Object data) {
checkIfCorrectlyEscaped(node, data);
@ -88,15 +86,18 @@ public class VfUnescapeElRule extends AbstractVfRule {
}
if (quoted) {
// check escaping too
if (!(jsonParse || escapeDetector.startsWithSafeResource(elExpression) || escapeDetector.containsSafeFields(elExpression))) {
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(elExpression,
if (!(jsonParse
|| ElEscapeDetector.startsWithSafeResource(elExpression)
|| ElEscapeDetector.containsSafeFields(elExpression))) {
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(elExpression,
JSENCODE_JSINHTMLENCODE)) {
addViolation(data, elExpression);
}
}
} else {
if (!(escapeDetector.startsWithSafeResource(elExpression) || escapeDetector.containsSafeFields(elExpression))) {
final boolean hasUnscaped = escapeDetector.doesElContainAnyUnescapedIdentifiers(elExpression,
if (!(ElEscapeDetector.startsWithSafeResource(elExpression)
|| ElEscapeDetector.containsSafeFields(elExpression))) {
final boolean hasUnscaped = ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(elExpression,
JSENCODE_JSINHTMLENCODE);
if (!(jsonParse && !hasUnscaped)) {
addViolation(data, elExpression);
@ -181,11 +182,11 @@ public class VfUnescapeElRule extends AbstractVfRule {
break;
}
if (escapeDetector.startsWithSafeResource(el)) {
if (ElEscapeDetector.startsWithSafeResource(el)) {
break;
}
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(el, ElEscapeDetector.Escaping.URLENCODE)) {
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el, ElEscapeDetector.Escaping.URLENCODE)) {
isEL = true;
toReport.add(el);
}
@ -216,12 +217,11 @@ public class VfUnescapeElRule extends AbstractVfRule {
if (ON_EVENT.matcher(name).matches()) {
final List<ASTElExpression> elsInVal = attr.findDescendantsOfType(ASTElExpression.class);
for (ASTElExpression el : elsInVal) {
if (escapeDetector.startsWithSafeResource(el)) {
if (ElEscapeDetector.startsWithSafeResource(el)) {
continue;
}
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(el,
ANY_ENCODE)) {
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el, ANY_ENCODE)) {
isEL = true;
toReport.add(el);
}
@ -280,11 +280,12 @@ public class VfUnescapeElRule extends AbstractVfRule {
final List<ASTElExpression> elsInVal = attr.findDescendantsOfType(ASTElExpression.class);
for (ASTElExpression el : elsInVal) {
if (escapeDetector.startsWithSafeResource(el)) {
if (ElEscapeDetector.startsWithSafeResource(el)) {
continue;
}
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(el, ElEscapeDetector.Escaping.HTMLENCODE)) {
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el,
ElEscapeDetector.Escaping.HTMLENCODE)) {
isEL = true;
toReport.add(el);
}
@ -347,11 +348,12 @@ public class VfUnescapeElRule extends AbstractVfRule {
for (ASTAttribute attrib : innerAttributes) {
final List<ASTElExpression> elsInVal = attrib.findDescendantsOfType(ASTElExpression.class);
for (final ASTElExpression el : elsInVal) {
if (escapeDetector.startsWithSafeResource(el)) {
if (ElEscapeDetector.startsWithSafeResource(el)) {
continue;
}
if (escapeDetector.doesElContainAnyUnescapedIdentifiers(el, ElEscapeDetector.Escaping.HTMLENCODE)) {
if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el,
ElEscapeDetector.Escaping.HTMLENCODE)) {
toReturn.add(el);
}
@ -363,5 +365,4 @@ public class VfUnescapeElRule extends AbstractVfRule {
return toReturn;
}
}

@ -1,4 +1,4 @@
/**
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
@ -18,7 +18,6 @@ import net.sourceforge.pmd.lang.vf.ast.ASTElExpression;
import net.sourceforge.pmd.lang.vf.ast.ASTExpression;
import net.sourceforge.pmd.lang.vf.ast.ASTIdentifier;
import net.sourceforge.pmd.lang.vf.ast.ASTNegationExpression;
import net.sourceforge.pmd.lang.vf.ast.AbstractVFNode;
import net.sourceforge.pmd.lang.vf.ast.VfNode;
import net.sourceforge.pmd.lang.vf.ast.VfTypedNode;
@ -30,14 +29,18 @@ import net.sourceforge.pmd.lang.vf.ast.VfTypedNode;
public final class ElEscapeDetector {
private static final Set<String> SAFE_EXPRESSIONS = new HashSet<>(Arrays.asList("id", "size", "caseNumber"));
private static final Set NON_EMPTY_ARG_SAFE_RESOURCE = new HashSet<>(Arrays.asList("urlfor", "casesafeid", "begins", "contains",
"len", "getrecordids", "linkto", "sqrt", "round", "mod", "log", "ln", "exp", "abs", "floor", "ceiling",
"nullvalue", "isnumber", "isnull", "isnew", "isblank", "isclone", "year", "month", "day", "datetimevalue",
"datevalue", "date", "now", "today"));
private static final Set EMPTY_ARG_SAFE_RESOURCE = new HashSet<>(Arrays.asList("$action", "$page", "$site",
private static final Set<String> NON_EMPTY_ARG_SAFE_RESOURCE = new HashSet<>(Arrays.asList("urlfor", "casesafeid",
"begins", "contains", "len", "getrecordids", "linkto", "sqrt", "round", "mod", "log", "ln", "exp", "abs",
"floor", "ceiling", "nullvalue", "isnumber", "isnull", "isnew", "isblank", "isclone", "year", "month",
"day", "datetimevalue", "datevalue", "date", "now", "today"));
private static final Set<String> EMPTY_ARG_SAFE_RESOURCE = new HashSet<>(Arrays.asList("$action", "$page", "$site",
"$resource", "$label", "$objecttype", "$component", "$remoteaction", "$messagechannel"));
public boolean innerContainsSafeFields(final VfNode expression) {
private ElEscapeDetector() {
// utility class
}
private static boolean innerContainsSafeFields(final VfNode expression) {
for (VfNode child : expression.children()) {
if (child instanceof ASTIdentifier && SAFE_EXPRESSIONS.contains(child.getImage().toLowerCase(Locale.ROOT))) {
@ -61,14 +64,13 @@ public final class ElEscapeDetector {
return false;
}
public boolean containsSafeFields(final AbstractVFNode expression) {
public static boolean containsSafeFields(final VfNode expression) {
final ASTExpression ex = expression.getFirstChildOfType(ASTExpression.class);
return ex != null && innerContainsSafeFields(ex);
}
public boolean startsWithSafeResource(final ASTElExpression el) {
public static boolean startsWithSafeResource(final ASTElExpression el) {
final ASTExpression expression = el.getFirstChildOfType(ASTExpression.class);
if (expression != null) {
final ASTNegationExpression negation = expression.getFirstChildOfType(ASTNegationExpression.class);
@ -94,12 +96,11 @@ public final class ElEscapeDetector {
return false;
}
public boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, Escaping escape) {
public static boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, Escaping escape) {
return doesElContainAnyUnescapedIdentifiers(elExpression, EnumSet.of(escape));
}
public boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression,
public static boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression,
EnumSet<Escaping> escapes) {
if (elExpression == null) {
return false;
@ -154,7 +155,7 @@ public final class ElEscapeDetector {
* Return true if the type of all data nodes can be determined and none of them require escaping
* @param expression
*/
public boolean expressionContainsSafeDataNodes(ASTExpression expression) {
private static boolean expressionContainsSafeDataNodes(ASTExpression expression) {
try {
for (VfTypedNode node : expression.getDataNodes().keySet()) {
DataType dataType = node.getDataType();
@ -187,5 +188,4 @@ public final class ElEscapeDetector {
return text;
}
}
}

@ -28,22 +28,34 @@ Avoid calling VF action upon page load as the action becomes vulnerable to CSRF.
<rule name="VfHtmlStyleTagXss"
language="vf"
since="6.29.0"
since="6.31.0"
message="Use correct encoding for expressions within Style tag"
class="net.sourceforge.pmd.lang.vf.rule.security.VfHtmlStyleTagXssRule"
externalInfoUrl="${pmd.website.baseurl}/pmd_rules_vf_security.html#vfhtmlstyletagxss">
<description>
Use relevant encoding with EL in html tags
Checks for the correct encoding in `&lt;style/&gt;` tags in Visualforce pages.
The rule is based on Salesforce Security's recommendation to prevent XSS in Visualforce as mentioned
on [Secure Coding Cross Site Scripting](https://developer.salesforce.com/docs/atlas.en-us.secure_coding_guide.meta/secure_coding_guide/secure_coding_cross_site_scripting.htm).
In order to avoid cross site scripting, the relevant encoding must be used in HTML tags. The rule expects
`URLENCODING` or `JSINHTMLENCODING` for URL-based style values and any kind of encoding
(e.g. `HTMLENCODING`) for non-url style values.
See also {% rule "VfUnescapeEl" %} to check escaping in other places on Visualforce pages.
</description>
<priority>3</priority>
<example>
<![CDATA[
<![CDATA[
<apex:page>
<style>
div {
background: url('{!XSSHere}');
}
</style>
<style>
div {
background: url('{!XSSHere}'); // Potential XSS
}
div {
background: url('{!URLENCODE(XSSHere)}'); // correct encoding
}
</style>
</apex:page>
]]>
</example>