Skip to content

Commit b62d73f

Browse files
nahsracarlosu7
andauthored
Add support for fixing SQL Injection in security hotspot (#384)
Co-authored-by: Carlos Uscanga <[email protected]>
1 parent 6ad5ea0 commit b62d73f

File tree

14 files changed

+473
-28
lines changed

14 files changed

+473
-28
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ public final class DefectDojoSqlInjectionCodemod extends JavaParserChanger
2525
implements FixOnlyCodeChanger {
2626

2727
private final RuleFindings findings;
28+
private final JavaParserSQLInjectionRemediatorStrategy remediatorStrategy;
2829

2930
@Inject
3031
public DefectDojoSqlInjectionCodemod(
3132
@DefectDojoScan(ruleId = "java.lang.security.audit.sqli.jdbc-sqli.jdbc-sqli")
3233
RuleFindings findings) {
3334
this.findings = Objects.requireNonNull(findings);
35+
this.remediatorStrategy = JavaParserSQLInjectionRemediatorStrategy.DEFAULT;
3436
}
3537

3638
@Override
@@ -50,12 +52,11 @@ public DetectorRule detectorRule() {
5052
public CodemodFileScanningResult visit(
5153
final CodemodInvocationContext context, final CompilationUnit cu) {
5254
List<Finding> findingsForThisPath = findings.getForPath(context.path());
53-
54-
return JavaParserSQLInjectionRemediatorStrategy.DEFAULT.visit(
55-
context,
55+
return remediatorStrategy.remediateAll(
5656
cu,
57-
findingsForThisPath,
57+
context.path().toString(),
5858
detectorRule(),
59+
findingsForThisPath,
5960
finding -> String.valueOf(finding.getId()),
6061
Finding::getLine);
6162
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.codemodder.codemods;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import io.codemodder.*;
5+
import io.codemodder.codemods.util.JavaParserSQLInjectionRemediatorStrategy;
6+
import io.codemodder.codetf.DetectorRule;
7+
import io.codemodder.providers.sonar.ProvidedSonarScan;
8+
import io.codemodder.providers.sonar.RuleHotspot;
9+
import io.codemodder.providers.sonar.SonarRemediatingJavaParserChanger;
10+
import io.codemodder.sonar.model.Hotspot;
11+
import io.codemodder.sonar.model.SonarFinding;
12+
import java.util.List;
13+
import java.util.Objects;
14+
import javax.inject.Inject;
15+
16+
@Codemod(
17+
id = "sonar:java/sonar-sql-injection-s2077",
18+
reviewGuidance = ReviewGuidance.MERGE_AFTER_REVIEW,
19+
importance = Importance.HIGH,
20+
executionPriority = CodemodExecutionPriority.HIGH)
21+
public final class SonarSQLInjectionCodemod extends SonarRemediatingJavaParserChanger {
22+
23+
private final JavaParserSQLInjectionRemediatorStrategy remediationStrategy;
24+
private final RuleHotspot hotspots;
25+
26+
@Inject
27+
public SonarSQLInjectionCodemod(
28+
@ProvidedSonarScan(ruleId = "java:S2077") final RuleHotspot hotspots) {
29+
this.hotspots = Objects.requireNonNull(hotspots);
30+
this.remediationStrategy = JavaParserSQLInjectionRemediatorStrategy.DEFAULT;
31+
}
32+
33+
@Override
34+
public DetectorRule detectorRule() {
35+
return new DetectorRule(
36+
"java:S2077",
37+
"Formatting SQL queries is security-sensitive",
38+
"https://rules.sonarsource.com/java/RSPEC-2077/");
39+
}
40+
41+
@Override
42+
public CodemodFileScanningResult visit(
43+
final CodemodInvocationContext context, final CompilationUnit cu) {
44+
List<Hotspot> hotspotsForFile = hotspots.getResultsByPath(context.path());
45+
return remediationStrategy.remediateAll(
46+
cu,
47+
context.path().toString(),
48+
detectorRule(),
49+
hotspotsForFile,
50+
SonarFinding::getKey,
51+
SonarFinding::getLine);
52+
}
53+
}

core-codemods/src/main/java/io/codemodder/codemods/util/DefaultJavaParserSQLInjectionRemediatorStrategy.java

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.github.javaparser.ast.expr.MethodCallExpr;
55
import io.codemodder.CodemodChange;
66
import io.codemodder.CodemodFileScanningResult;
7-
import io.codemodder.CodemodInvocationContext;
87
import io.codemodder.codemods.SQLParameterizer;
98
import io.codemodder.codemods.SQLParameterizerWithCleanup;
109
import io.codemodder.codetf.DetectorRule;
@@ -25,40 +24,40 @@ final class DefaultJavaParserSQLInjectionRemediatorStrategy
2524
/**
2625
* Visits the provided CompilationUnit and processes findings for potential SQL injections.
2726
*
28-
* @param context the context of the codemod invocation
2927
* @param cu the compilation unit to be scanned
30-
* @param pathFindings a collection of findings to be processed
31-
* @param detectorRule the rule used to detect potential issues
28+
* @param path the path of the file being scanned
29+
* @param detectorRule the detector rule that generated the findings
30+
* @param findingsForPath a collection of findings to be processed
3231
* @param findingIdExtractor a function to extract the ID from a finding
3332
* @param findingLineExtractor a function to extract the line number from a finding
3433
* @param <T> the type of the findings
3534
* @return a result object containing the changes and unfixed findings
3635
*/
37-
public <T> CodemodFileScanningResult visit(
38-
final CodemodInvocationContext context,
36+
@Override
37+
public <T> CodemodFileScanningResult remediateAll(
3938
final CompilationUnit cu,
40-
final Collection<T> pathFindings,
39+
final String path,
4140
final DetectorRule detectorRule,
41+
final Collection<T> findingsForPath,
4242
final Function<T, String> findingIdExtractor,
4343
final Function<T, Integer> findingLineExtractor) {
4444

4545
final List<MethodCallExpr> allMethodCalls = cu.findAll(MethodCallExpr.class);
4646

47-
if (pathFindings.isEmpty()) {
47+
if (findingsForPath.isEmpty()) {
4848
return CodemodFileScanningResult.none();
4949
}
5050

5151
final List<UnfixedFinding> unfixedFindings = new ArrayList<>();
5252
final List<CodemodChange> changes = new ArrayList<>();
5353

54-
for (T finding : pathFindings) {
54+
for (T finding : findingsForPath) {
5555
final String id = findingIdExtractor.apply(finding);
5656
final Integer line = findingLineExtractor.apply(finding);
5757

5858
if (line == null) {
5959
final UnfixedFinding unfixableFinding =
60-
new UnfixedFinding(
61-
id, detectorRule, context.path().toString(), null, "No line number provided");
60+
new UnfixedFinding(id, detectorRule, path, null, "No line number provided");
6261
unfixedFindings.add(unfixableFinding);
6362
continue;
6463
}
@@ -72,11 +71,7 @@ public <T> CodemodFileScanningResult visit(
7271
if (supportedSqlMethodCallsOnThatLine.isEmpty()) {
7372
final UnfixedFinding unfixableFinding =
7473
new UnfixedFinding(
75-
id,
76-
detectorRule,
77-
context.path().toString(),
78-
line,
79-
"No supported SQL methods found on the given line");
74+
id, detectorRule, path, line, "No supported SQL methods found on the given line");
8075
unfixedFindings.add(unfixableFinding);
8176
continue;
8277
}
@@ -86,7 +81,7 @@ public <T> CodemodFileScanningResult visit(
8681
new UnfixedFinding(
8782
id,
8883
detectorRule,
89-
context.path().toString(),
84+
path,
9085
line,
9186
"Multiple supported SQL methods found on the given line");
9287
unfixedFindings.add(unfixableFinding);
@@ -101,7 +96,7 @@ public <T> CodemodFileScanningResult visit(
10196
new UnfixedFinding(
10297
id,
10398
detectorRule,
104-
context.path().toString(),
99+
path,
105100
line,
106101
"State changing effects possible or unrecognized code shape");
107102
unfixedFindings.add(unfixableFinding);

core-codemods/src/main/java/io/codemodder/codemods/util/JavaParserSQLInjectionRemediatorStrategy.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.github.javaparser.ast.CompilationUnit;
44
import io.codemodder.CodemodFileScanningResult;
5-
import io.codemodder.CodemodInvocationContext;
65
import io.codemodder.codetf.DetectorRule;
76
import java.util.Collection;
87
import java.util.function.Function;
@@ -17,23 +16,22 @@ public interface JavaParserSQLInjectionRemediatorStrategy {
1716
/**
1817
* Visits the provided CompilationUnit and processes findings for potential SQL injections.
1918
*
20-
* @param context the context of the codemod invocation
2119
* @param cu the compilation unit to be scanned
2220
* @param pathFindings a collection of findings to be processed
23-
* @param detectorRule the rule used to detect potential issues
2421
* @param findingIdExtractor a function to extract the ID from a finding
2522
* @param findingLineExtractor a function to extract the line number from a finding
2623
* @param <T> the type of the findings
2724
* @return a result object containing the changes and unfixed findings
2825
*/
29-
<T> CodemodFileScanningResult visit(
30-
final CodemodInvocationContext context,
26+
<T> CodemodFileScanningResult remediateAll(
3127
final CompilationUnit cu,
28+
final String path,
29+
final DetectorRule rule,
3230
final Collection<T> pathFindings,
33-
final DetectorRule detectorRule,
3431
final Function<T, String> findingIdExtractor,
3532
final Function<T, Integer> findingLineExtractor);
3633

34+
/** A default implementation that should be used in all non-test scenarios. */
3735
JavaParserSQLInjectionRemediatorStrategy DEFAULT =
3836
new DefaultJavaParserSQLInjectionRemediatorStrategy();
3937
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This change refactors SQL statements to be parameterized, rather than built by hand.
2+
3+
Without parameterization, developers must remember to escape inputs using the rules for that database. It's usually buggy, at the least -- and sometimes vulnerable.
4+
5+
Our changes look something like this:
6+
7+
```diff
8+
- Statement stmt = connection.createStatement();
9+
- ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE name = '" + user + "'");
10+
+ PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE name = ?");
11+
+ stmt.setString(1, user);
12+
+ ResultSet rs = stmt.executeQuery();
13+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"summary" : "Refactored to use parameterized SQL APIs",
3+
"change": "Parameterized SQL usage to prevent any bugs or vulnerabilities",
4+
"reviewGuidanceIJustification" : "Although there should be no functional differences, the rewrite here is complex and should be verified by a human.",
5+
"references" : [
6+
"https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html",
7+
"https://cwe.mitre.org/data/definitions/89.html"
8+
]
9+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.codemodder.codemods;
2+
3+
import io.codemodder.testutils.CodemodTestMixin;
4+
import io.codemodder.testutils.Metadata;
5+
import org.junit.jupiter.api.Nested;
6+
7+
final class SonarSQLInjectionCodemodTest {
8+
9+
@Nested
10+
@Metadata(
11+
codemodType = SonarSQLInjectionCodemod.class,
12+
testResourceDir = "sonar-sql-injection-s2077/unsupported",
13+
renameTestFile = "src/main/java/org/owasp/webgoat/container/users/UserService.java",
14+
expectingFailedFixesAtLines = {52}, // we don't support this method
15+
dependencies = {})
16+
class UnsupportedTest implements CodemodTestMixin {}
17+
18+
@Nested
19+
@Metadata(
20+
codemodType = SonarSQLInjectionCodemod.class,
21+
testResourceDir = "sonar-sql-injection-s2077/supported",
22+
renameTestFile =
23+
"src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionChallenge.java",
24+
expectingFixesAtLines = {69},
25+
dependencies = {})
26+
class SupportedTest implements CodemodTestMixin {}
27+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* This file is part of WebGoat, an Open Web Application Security Project utility. For details, please see http://www.owasp.org/
3+
*
4+
* Copyright (c) 2002 - 2019 Bruce Mayhew
5+
*
6+
* This program is free software; you can redistribute it and/or modify it under the terms of the
7+
* GNU General Public License as published by the Free Software Foundation; either version 2 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
11+
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License along with this program; if
15+
* not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
16+
* 02111-1307, USA.
17+
*
18+
* Getting Source ==============
19+
*
20+
* Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository for free software projects.
21+
*/
22+
23+
package org.owasp.webgoat.lessons.sqlinjection.advanced;
24+
25+
import java.sql.*;
26+
import java.sql.PreparedStatement;
27+
import lombok.extern.slf4j.Slf4j;
28+
import org.owasp.webgoat.container.LessonDataSource;
29+
import org.owasp.webgoat.container.assignments.AssignmentEndpoint;
30+
import org.owasp.webgoat.container.assignments.AssignmentHints;
31+
import org.owasp.webgoat.container.assignments.AttackResult;
32+
import org.springframework.util.StringUtils;
33+
import org.springframework.web.bind.annotation.PutMapping;
34+
import org.springframework.web.bind.annotation.RequestParam;
35+
import org.springframework.web.bind.annotation.ResponseBody;
36+
import org.springframework.web.bind.annotation.RestController;
37+
38+
/**
39+
* @author nbaars
40+
* @since 4/8/17.
41+
*/
42+
@RestController
43+
@AssignmentHints(
44+
value = {"SqlInjectionChallenge1", "SqlInjectionChallenge2", "SqlInjectionChallenge3"})
45+
@Slf4j
46+
public class SqlInjectionChallenge extends AssignmentEndpoint {
47+
48+
private final LessonDataSource dataSource;
49+
50+
public SqlInjectionChallenge(LessonDataSource dataSource) {
51+
this.dataSource = dataSource;
52+
}
53+
54+
@PutMapping("/SqlInjectionAdvanced/challenge")
55+
// assignment path is bounded to class so we use different http method :-)
56+
@ResponseBody
57+
public AttackResult registerNewUser(
58+
@RequestParam String username_reg,
59+
@RequestParam String email_reg,
60+
@RequestParam String password_reg)
61+
throws Exception {
62+
AttackResult attackResult = checkArguments(username_reg, email_reg, password_reg);
63+
64+
if (attackResult == null) {
65+
66+
try (Connection connection = dataSource.getConnection()) {
67+
String checkUserQuery =
68+
"select userid from sql_challenge_users where userid = ?";
69+
PreparedStatement statement = connection.prepareStatement(checkUserQuery);
70+
statement.setString(1, username_reg);
71+
ResultSet resultSet = statement.execute();
72+
if (resultSet.next()) {
73+
if (username_reg.contains("tom'")) {
74+
attackResult = success(this).feedback("user.exists").build();
75+
} else {
76+
attackResult = failed(this).feedback("user.exists").feedbackArgs(username_reg).build();
77+
}
78+
} else {
79+
PreparedStatement preparedStatement =
80+
connection.prepareStatement("INSERT INTO sql_challenge_users VALUES (?, ?, ?)");
81+
preparedStatement.setString(1, username_reg);
82+
preparedStatement.setString(2, email_reg);
83+
preparedStatement.setString(3, password_reg);
84+
preparedStatement.execute();
85+
attackResult = success(this).feedback("user.created").feedbackArgs(username_reg).build();
86+
}
87+
88+
} catch (SQLException e) {
89+
attackResult = failed(this).output("Something went wrong").build();
90+
}
91+
}
92+
return attackResult;
93+
}
94+
95+
private AttackResult checkArguments(String username_reg, String email_reg, String password_reg) {
96+
if (StringUtils.isEmpty(username_reg)
97+
|| StringUtils.isEmpty(email_reg)
98+
|| StringUtils.isEmpty(password_reg)) {
99+
return failed(this).feedback("input.invalid").build();
100+
}
101+
if (username_reg.length() > 250 || email_reg.length() > 30 || password_reg.length() > 30) {
102+
return failed(this).feedback("input.invalid").build();
103+
}
104+
return null;
105+
}
106+
}

0 commit comments

Comments
 (0)