Skip to content

Commit d9b49a0

Browse files
authored
Improve XSS fixer and create CodeQL mapping (#467)
1 parent 0214068 commit d9b49a0

File tree

6 files changed

+222
-23
lines changed

6 files changed

+222
-23
lines changed

core-codemods/src/main/java/io/codemodder/codemods/DefaultCodemods.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static List<Class<? extends CodeChanger>> asList() {
4141
CodeQLSSRFCodemod.class,
4242
CodeQLStackTraceExposureCodemod.class,
4343
CodeQLUnverifiedJwtCodemod.class,
44+
CodeQLXSSCodemod.class,
4445
CodeQLXXECodemod.class,
4546
DeclareVariableOnSeparateLineCodemod.class,
4647
DefectDojoSqlInjectionCodemod.class,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.codemodder.codemods.codeql;
2+
3+
import com.contrastsecurity.sarif.Result;
4+
import com.github.javaparser.ast.CompilationUnit;
5+
import io.codemodder.*;
6+
import io.codemodder.codetf.DetectorRule;
7+
import io.codemodder.providers.sarif.codeql.ProvidedCodeQLScan;
8+
import io.codemodder.remediation.GenericRemediationMetadata;
9+
import io.codemodder.remediation.Remediator;
10+
import io.codemodder.remediation.xss.XSSRemediator;
11+
import java.util.Optional;
12+
import javax.inject.Inject;
13+
14+
/** A codemod for automatically fixing XSS from CodeQL. */
15+
@Codemod(
16+
id = "codeql:java/xss",
17+
reviewGuidance = ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
18+
importance = Importance.HIGH,
19+
executionPriority = CodemodExecutionPriority.HIGH)
20+
public final class CodeQLXSSCodemod extends CodeQLRemediationCodemod {
21+
22+
private final Remediator<Result> remediator;
23+
24+
@Inject
25+
public CodeQLXSSCodemod(@ProvidedCodeQLScan(ruleId = "java/xss") final RuleSarif sarif) {
26+
super(GenericRemediationMetadata.XSS.reporter(), sarif);
27+
this.remediator = new XSSRemediator<>();
28+
}
29+
30+
@Override
31+
public DetectorRule detectorRule() {
32+
return new DetectorRule(
33+
"xss",
34+
"Cross-site scripting",
35+
"https://codeql.github.com/codeql-query-help/java/java-xss/");
36+
}
37+
38+
@Override
39+
public CodemodFileScanningResult visit(
40+
final CodemodInvocationContext context, final CompilationUnit cu) {
41+
return remediator.remediateAll(
42+
cu,
43+
context.path().toString(),
44+
detectorRule(),
45+
ruleSarif.getResultsByLocationPath(context.path()),
46+
SarifFindingKeyUtil::buildFindingId,
47+
r -> r.getLocations().get(0).getPhysicalLocation().getRegion().getStartLine(),
48+
r ->
49+
Optional.ofNullable(
50+
r.getLocations().get(0).getPhysicalLocation().getRegion().getEndLine()),
51+
r ->
52+
Optional.ofNullable(
53+
r.getLocations().get(0).getPhysicalLocation().getRegion().getStartColumn()));
54+
}
55+
}

framework/codemodder-base/src/main/java/io/codemodder/remediation/xss/NakedVariableReturnFixStrategy.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
import io.codemodder.remediation.SuccessOrReason;
1111
import java.util.List;
1212
import java.util.Optional;
13-
import org.jetbrains.annotations.VisibleForTesting;
1413

14+
/**
15+
* Fix strategy for XSS vulnerabilities where a variable is returned directly and that is what's
16+
* vulnerable.
17+
*/
1518
final class NakedVariableReturnFixStrategy implements RemediationStrategy {
19+
1620
@Override
1721
public SuccessOrReason fix(final CompilationUnit cu, final Node node) {
1822
var maybeReturn = Optional.of(node).map(n -> n instanceof ReturnStmt ? (ReturnStmt) n : null);
@@ -25,8 +29,7 @@ public SuccessOrReason fix(final CompilationUnit cu, final Node node) {
2529
return SuccessOrReason.success(List.of(DependencyGAV.OWASP_XSS_JAVA_ENCODER));
2630
}
2731

28-
@VisibleForTesting
29-
public static boolean match(final Node node) {
32+
static boolean match(final Node node) {
3033
return Optional.of(node)
3134
.map(n -> n instanceof ReturnStmt ? (ReturnStmt) n : null)
3235
.filter(rs -> rs.getExpression().isPresent())

framework/codemodder-base/src/main/java/io/codemodder/remediation/xss/PrintingMethodFixStrategy.java

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44

55
import com.github.javaparser.ast.CompilationUnit;
66
import com.github.javaparser.ast.Node;
7+
import com.github.javaparser.ast.expr.BinaryExpr;
8+
import com.github.javaparser.ast.expr.Expression;
79
import com.github.javaparser.ast.expr.MethodCallExpr;
810
import io.codemodder.DependencyGAV;
911
import io.codemodder.remediation.RemediationStrategy;
1012
import io.codemodder.remediation.SuccessOrReason;
1113
import java.util.List;
1214
import java.util.Optional;
1315
import java.util.Set;
14-
import org.jetbrains.annotations.VisibleForTesting;
1516

17+
/** Fix strategy for XSS vulnerabilities where a variable is sent to a simple print/write call. */
1618
final class PrintingMethodFixStrategy implements RemediationStrategy {
1719

1820
@Override
@@ -23,14 +25,55 @@ public SuccessOrReason fix(final CompilationUnit cu, final Node node) {
2325
return SuccessOrReason.reason("Not a method call.");
2426
}
2527
MethodCallExpr call = maybeCall.get();
26-
wrap(call.getArgument(0)).withStaticMethod("org.owasp.encoder.Encode", "forHtml", false);
28+
29+
Expression methodArgument = call.getArgument(0);
30+
31+
Optional<Expression> thingToWrap = findExpressionToWrap(methodArgument);
32+
if (thingToWrap.isEmpty()) {
33+
return SuccessOrReason.reason("Could not find recognize code shape to fix.");
34+
}
35+
Expression expressionToWrap = thingToWrap.get();
36+
wrap(expressionToWrap).withStaticMethod("org.owasp.encoder.Encode", "forHtml", false);
2737
return SuccessOrReason.success(List.of(DependencyGAV.OWASP_XSS_JAVA_ENCODER));
2838
}
2939

40+
/**
41+
* We handle 4 expression code shapes. <code>
42+
* print(user.getName());
43+
* print("Hello, " + user.getName());
44+
* print(user.getName() + ", hello!");
45+
* print("Hello, " + user.getName() + ", hello!");
46+
* </code>
47+
*
48+
* <p>Note that we should only handle, for the tougher cases, string literals in combination with
49+
* the given expression. Note any other combination of expressions.
50+
*/
51+
private Optional<Expression> findExpressionToWrap(final Expression expression) {
52+
if (expression.isNameExpr()) {
53+
return Optional.of(expression);
54+
} else if (expression.isBinaryExpr()) {
55+
BinaryExpr binaryExpr = expression.asBinaryExpr();
56+
if (binaryExpr.getLeft().isBinaryExpr() && binaryExpr.getRight().isStringLiteralExpr()) {
57+
BinaryExpr leftBinaryExpr = binaryExpr.getLeft().asBinaryExpr();
58+
if (leftBinaryExpr.getLeft().isStringLiteralExpr()
59+
&& !leftBinaryExpr.getRight().isStringLiteralExpr()) {
60+
return Optional.of(leftBinaryExpr.getRight());
61+
}
62+
} else if (binaryExpr.getLeft().isStringLiteralExpr()
63+
&& binaryExpr.getRight().isStringLiteralExpr()) {
64+
return Optional.empty();
65+
} else if (binaryExpr.getLeft().isStringLiteralExpr()) {
66+
return Optional.of(binaryExpr.getRight());
67+
} else if (binaryExpr.getRight().isStringLiteralExpr()) {
68+
return Optional.of(binaryExpr.getLeft());
69+
}
70+
}
71+
return Optional.empty();
72+
}
73+
3074
private static final Set<String> writingMethodNames = Set.of("print", "println", "write");
3175

32-
@VisibleForTesting
33-
public static boolean match(final Node node) {
76+
static boolean match(final Node node) {
3477
return Optional.of(node)
3578
.map(n -> n instanceof MethodCallExpr ? (MethodCallExpr) n : null)
3679
.filter(mce -> writingMethodNames.contains(mce.getNameAsString()))

framework/codemodder-base/src/main/java/io/codemodder/remediation/xss/XSSRemediator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import java.util.Optional;
1111
import java.util.function.Function;
1212

13-
public class XSSRemediator<T> implements Remediator<T> {
13+
/** Remediator for XSS vulnerabilities. */
14+
public final class XSSRemediator<T> implements Remediator<T> {
1415

1516
private final SearcherStrategyRemediator<T> searchStrategyRemediator;
1617

framework/codemodder-base/src/test/java/io/codemodder/remediation/xss/PrintingMethodFixerTest.java

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.github.javaparser.StaticJavaParser;
66
import com.github.javaparser.ast.CompilationUnit;
77
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
8+
import io.codemodder.CodemodFileScanningResult;
89
import io.codemodder.codetf.DetectorRule;
910
import io.codemodder.remediation.FixCandidateSearcher;
1011
import io.codemodder.remediation.SearcherStrategyRemediator;
@@ -76,7 +77,55 @@ void should_be_fixed(String s) {
7677
getWriter().write(Encode.forHtml(s));
7778
}
7879
}
79-
"""));
80+
"""),
81+
Arguments.of(
82+
"""
83+
class Samples {
84+
void should_be_fixed(String s) {
85+
getWriter().write("<div>" + s);
86+
}
87+
}
88+
""",
89+
"""
90+
import org.owasp.encoder.Encode;
91+
class Samples {
92+
void should_be_fixed(String s) {
93+
getWriter().write("<div>" + Encode.forHtml(s));
94+
}
95+
}
96+
"""),
97+
Arguments.of(
98+
"""
99+
class Samples {
100+
void should_be_fixed(String s) {
101+
getWriter().write("<div>" + s + "</div>");
102+
}
103+
}
104+
""",
105+
"""
106+
import org.owasp.encoder.Encode;
107+
class Samples {
108+
void should_be_fixed(String s) {
109+
getWriter().write("<div>" + Encode.forHtml(s) + "</div>");
110+
}
111+
}
112+
"""),
113+
Arguments.of(
114+
"""
115+
class Samples {
116+
void should_be_fixed(String s) {
117+
getWriter().write(s + "</div>");
118+
}
119+
}
120+
""",
121+
"""
122+
import org.owasp.encoder.Encode;
123+
class Samples {
124+
void should_be_fixed(String s) {
125+
getWriter().write(Encode.forHtml(s) + "</div>");
126+
}
127+
}
128+
"""));
80129
}
81130

82131
@ParameterizedTest
@@ -85,7 +134,14 @@ void it_fixes_obvious_response_write_methods(final String beforeCode, final Stri
85134
CompilationUnit cu = StaticJavaParser.parse(beforeCode);
86135
LexicalPreservingPrinter.setup(cu);
87136

88-
XSSFinding finding = new XSSFinding("should_be_fixed", 3, null);
137+
var result = scanAndFix(cu, 3);
138+
assertThat(result.changes()).isNotEmpty();
139+
String actualCode = LexicalPreservingPrinter.print(cu);
140+
assertThat(actualCode).isEqualToIgnoringWhitespace(afterCode);
141+
}
142+
143+
private CodemodFileScanningResult scanAndFix(final CompilationUnit cu, final int line) {
144+
XSSFinding finding = new XSSFinding("should_be_fixed", line, null);
89145
var remediator =
90146
new SearcherStrategyRemediator.Builder<XSSFinding>()
91147
.withSearcherStrategyPair(
@@ -94,18 +150,58 @@ void it_fixes_obvious_response_write_methods(final String beforeCode, final Stri
94150
.build(),
95151
fixer)
96152
.build();
97-
var result =
98-
remediator.remediateAll(
99-
cu,
100-
"path",
101-
rule,
102-
List.of(finding),
103-
XSSFinding::key,
104-
XSSFinding::line,
105-
x -> Optional.empty(),
106-
x -> Optional.ofNullable(x.column()));
107-
assertThat(result.changes().isEmpty()).isFalse();
108-
String actualCode = LexicalPreservingPrinter.print(cu);
109-
assertThat(actualCode).isEqualToIgnoringWhitespace(afterCode);
153+
return remediator.remediateAll(
154+
cu,
155+
"path",
156+
rule,
157+
List.of(finding),
158+
XSSFinding::key,
159+
XSSFinding::line,
160+
x -> Optional.empty(),
161+
x -> Optional.ofNullable(x.column()));
162+
}
163+
164+
@ParameterizedTest
165+
@MethodSource("unfixableSamples")
166+
void it_does_not_fix_unfixable_response_write_methods(final String beforeCode, final int line) {
167+
CompilationUnit cu = StaticJavaParser.parse(beforeCode);
168+
LexicalPreservingPrinter.setup(cu);
169+
var result = scanAndFix(cu, line);
170+
assertThat(result.changes()).isEmpty();
171+
}
172+
173+
private static Stream<Arguments> unfixableSamples() {
174+
return Stream.of(
175+
// this is all string literals -- ignore
176+
Arguments.of(
177+
"""
178+
class Samples {
179+
void should_be_fixed(String s) {
180+
getWriter().write("<div>" + "<b>" + "</div>");
181+
}
182+
}
183+
""",
184+
3),
185+
// this is ambiguous which value to encode
186+
Arguments.of(
187+
"""
188+
class Samples {
189+
void should_be_fixed(String s) {
190+
getWriter().write("<div>" + a + b + "</div>");
191+
}
192+
}
193+
""",
194+
3),
195+
// this is the wrong line
196+
Arguments.of(
197+
"""
198+
class Samples {
199+
void should_be_fixed(String s) {
200+
// extra line, right line is 4
201+
getWriter().write("<div>" + a + "</div>");
202+
}
203+
}
204+
""",
205+
3));
110206
}
111207
}

0 commit comments

Comments
 (0)