forked from phoedos/pmd
Fixups for #3005
- ElEscapeDetector is utility class now - Improved description and example of new rule
This commit is contained in:
pmd-visualforce/src/main
java/net/sourceforge/pmd/lang/vf/rule/security
resources/category/vf
24
pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfHtmlStyleTagXssRule.java
24
pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfHtmlStyleTagXssRule.java
@ -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>
|
||||
* <style>
|
||||
* div {
|
||||
* background: url('{!HTMLENCODE(XSSHere)}');
|
||||
* }
|
||||
* </style>
|
||||
* </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 `<style/>` 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>
|
||||
|
Reference in New Issue
Block a user