/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.apex.rule.security;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.sourceforge.pmd.lang.apex.ast.ASTAssignmentExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTBinaryExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
import net.sourceforge.pmd.lang.apex.ast.ASTMethodCallExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTReturnStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTUserClass;
import net.sourceforge.pmd.lang.apex.ast.ASTVariableDeclaration;
import net.sourceforge.pmd.lang.apex.ast.ASTVariableExpression;
import net.sourceforge.pmd.lang.apex.ast.AbstractApexNode;
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule;
/**
* Detects potential XSS when controller extracts a variable from URL query and
* uses it without escaping first
*
* @author sergey.gorbaty
*
*/
public class ApexXSSFromURLParamRule extends AbstractApexRule {
private static final String[] URL_PARAMETER_METHOD = new String[] { "ApexPages", "currentPage", "getParameters",
"get", };
private static final String[] HTML_ESCAPING = new String[] { "ESAPI", "encoder", "SFDC_HTMLENCODE" };
private static final String[] JS_ESCAPING = new String[] { "ESAPI", "encoder", "SFDC_JSENCODE" };
private static final String[] JSINHTML_ESCAPING = new String[] { "ESAPI", "encoder", "SFDC_JSINHTMLENCODE" };
private static final String[] URL_ESCAPING = new String[] { "ESAPI", "encoder", "SFDC_URLENCODE" };
private static final String[] STRING_HTML3 = new String[] { "String", "escapeHtml3" };
private static final String[] STRING_HTML4 = new String[] { "String", "escapeHtml4" };
private static final String[] STRING_XML = new String[] { "String", "escapeXml" };
private static final String[] STRING_ECMASCRIPT = new String[] { "String", "escapeEcmaScript" };
private static final String[] INTEGER_VALUEOF = new String[] { "Integer", "valueOf" };
private static final String[] ID_VALUEOF = new String[] { "ID", "valueOf" };
private static final String[] DOUBLE_VALUEOF = new String[] { "Double", "valueOf" };
private static final String[] BOOLEAN_VALUEOF = new String[] { "Boolean", "valueOf" };
private static final String[] STRING_ISEMPTY = new String[] { "String", "isEmpty" };
private static final String[] STRING_ISBLANK = new String[] { "String", "isBlank" };
private static final String[] STRING_ISNOTBLANK = new String[] { "String", "isNotBlank" };
private final Set<String> urlParameterStrings = new HashSet<>();
public ApexXSSFromURLParamRule() {
setProperty(CODECLIMATE_CATEGORIES, new String[] { "Security" });
setProperty(CODECLIMATE_REMEDIATION_MULTIPLIER, 50);
setProperty(CODECLIMATE_BLOCK_HIGHLIGHTING, false);
}
@Override
public Object visit(ASTUserClass node, Object data) {
if (Helper.isTestMethodOrClass(node) || Helper.isSystemLevelClass(node)) {
return data; // stops all the rules
}
return visit((ApexNode<?>) node, data);
}
@Override
public Object visit(ASTAssignmentExpression node, Object data) {
findTaintedVariables(node, data);
processVariableAssignments(node, data, false);
return data;
}
@Override
public Object visit(ASTVariableDeclaration node, Object data) {
findTaintedVariables(node, data);
processVariableAssignments(node, data, true);
return data;
}
@Override
public Object visit(ASTFieldDeclaration node, Object data) {
findTaintedVariables(node, data);
processVariableAssignments(node, data, true);
return data;
}
@Override
public Object visit(ASTMethodCallExpression node, Object data) {
processEscapingMethodCalls(node, data);
processInlineMethodCalls(node, data, false);
return data;
}
@Override
public Object visit(ASTReturnStatement node, Object data) {
ASTBinaryExpression binaryExpression = node.getFirstChildOfType(ASTBinaryExpression.class);
if (binaryExpression != null) {
processBinaryExpression(binaryExpression, data);
}
ASTMethodCallExpression methodCall = node.getFirstChildOfType(ASTMethodCallExpression.class);
if (methodCall != null) {
String retType = getReturnType(node);
if ("string".equalsIgnoreCase(retType)) {
processInlineMethodCalls(methodCall, data, true);
}
}
List<ASTVariableExpression> nodes = node.findChildrenOfType(ASTVariableExpression.class);
for (ASTVariableExpression varExpression : nodes) {
if (urlParameterStrings.contains(Helper.getFQVariableName(varExpression))) {
addViolation(data, nodes.get(0));
}
}
return data;
}
private String getReturnType(ASTReturnStatement node) {
ASTMethod method = node.getFirstParentOfType(ASTMethod.class);
if (method != null) {
return method.getNode().getMethodInfo().getReturnType().getApexName();
}
return "";
}
private boolean isEscapingMethod(ASTMethodCallExpression methodNode) {
// escaping methods
return Helper.isMethodCallChain(methodNode, HTML_ESCAPING) || Helper.isMethodCallChain(methodNode, JS_ESCAPING)
|| Helper.isMethodCallChain(methodNode, JSINHTML_ESCAPING)
|| Helper.isMethodCallChain(methodNode, URL_ESCAPING)
|| Helper.isMethodCallChain(methodNode, STRING_HTML3)
|| Helper.isMethodCallChain(methodNode, STRING_HTML4)
|| Helper.isMethodCallChain(methodNode, STRING_XML)
|| Helper.isMethodCallChain(methodNode, STRING_ECMASCRIPT)
// safe casts that eliminate injection
|| Helper.isMethodCallChain(methodNode, INTEGER_VALUEOF)
|| Helper.isMethodCallChain(methodNode, DOUBLE_VALUEOF)
|| Helper.isMethodCallChain(methodNode, BOOLEAN_VALUEOF)
|| Helper.isMethodCallChain(methodNode, ID_VALUEOF)
// safe boolean methods
|| Helper.isMethodCallChain(methodNode, STRING_ISEMPTY)
|| Helper.isMethodCallChain(methodNode, STRING_ISBLANK)
|| Helper.isMethodCallChain(methodNode, STRING_ISNOTBLANK);
}
private void processInlineMethodCalls(ASTMethodCallExpression methodNode, Object data, final boolean isNested) {
ASTMethodCallExpression nestedCall = methodNode.getFirstChildOfType(ASTMethodCallExpression.class);
if (nestedCall != null) {
if (!isEscapingMethod(methodNode)) {
processInlineMethodCalls(nestedCall, data, true);
}
}
if (Helper.isMethodCallChain(methodNode, URL_PARAMETER_METHOD)) {
if (isNested) {
addViolation(data, methodNode);
}
}
}
private void findTaintedVariables(AbstractApexNode<?> node, Object data) {
final ASTMethodCallExpression right = node.getFirstChildOfType(ASTMethodCallExpression.class);
// Looks for: (String) foo =
// ApexPages.currentPage().getParameters().get(..)
if (right != null) {
if (Helper.isMethodCallChain(right, URL_PARAMETER_METHOD)) {
ASTVariableExpression left = node.getFirstChildOfType(ASTVariableExpression.class);
String varType = null;
if (node instanceof ASTVariableDeclaration) {
varType = ((ASTVariableDeclaration) node).getNode().getLocalInfo().getType().getApexName();
}
if (left != null) {
if (varType == null || !"id".equalsIgnoreCase(varType)) {
urlParameterStrings.add(Helper.getFQVariableName(left));
}
}
}
processEscapingMethodCalls(right, data);
}
}
private void processEscapingMethodCalls(ASTMethodCallExpression methodNode, Object data) {
ASTMethodCallExpression nestedCall = methodNode.getFirstChildOfType(ASTMethodCallExpression.class);
if (nestedCall != null) {
processEscapingMethodCalls(nestedCall, data);
}
final ASTVariableExpression variable = methodNode.getFirstChildOfType(ASTVariableExpression.class);
if (variable != null) {
if (urlParameterStrings.contains(Helper.getFQVariableName(variable))) {
if (!isEscapingMethod(methodNode)) {
addViolation(data, variable);
}
}
}
}
private void processVariableAssignments(AbstractApexNode<?> node, Object data, final boolean reverseOrder) {
ASTMethodCallExpression methodCallAssignment = node.getFirstChildOfType(ASTMethodCallExpression.class);
if (methodCallAssignment != null) {
String varType = null;
if (node instanceof ASTVariableDeclaration) {
varType = ((ASTVariableDeclaration) node).getNode().getLocalInfo().getType().getApexName();
}
if (varType == null || !"id".equalsIgnoreCase(varType)) {
processInlineMethodCalls(methodCallAssignment, data, false);
}
}
List<ASTVariableExpression> nodes = node.findChildrenOfType(ASTVariableExpression.class);
switch (nodes.size()) {
case 1: {
// Look for: foo + bar
final List<ASTBinaryExpression> ops = node.findChildrenOfType(ASTBinaryExpression.class);
if (!ops.isEmpty()) {
for (ASTBinaryExpression o : ops) {
processBinaryExpression(o, data);
}
}
}
break;
case 2: {
// Look for: foo = bar;
final ASTVariableExpression right = reverseOrder ? nodes.get(0) : nodes.get(1);
if (urlParameterStrings.contains(Helper.getFQVariableName(right))) {
addViolation(data, right);
}
}
break;
default:
break;
}
}
private void processBinaryExpression(AbstractApexNode<?> node, Object data) {
ASTBinaryExpression nestedBinaryExpression = node.getFirstChildOfType(ASTBinaryExpression.class);
if (nestedBinaryExpression != null) {
processBinaryExpression(nestedBinaryExpression, data);
}
ASTMethodCallExpression methodCallAssignment = node.getFirstChildOfType(ASTMethodCallExpression.class);
if (methodCallAssignment != null) {
processInlineMethodCalls(methodCallAssignment, data, true);
}
final List<ASTVariableExpression> nodes = node.findChildrenOfType(ASTVariableExpression.class);
for (ASTVariableExpression n : nodes) {
if (urlParameterStrings.contains(Helper.getFQVariableName(n))) {
addViolation(data, n);
}
}
}
}