Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes issue where multiple mixed type injections were not fixed #434

Merged
merged 2 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ class SupportedTest implements CodemodTestMixin {}
expectingFixesAtLines = {19, 25, 33, 40},
dependencies = {})
class SupportedTableInjectionTest implements CodemodTestMixin {}

@Nested
@Metadata(
codemodType = SonarSQLInjectionCodemod.class,
testResourceDir = "sonar-sql-injection-s2077/supportedMixedInjections",
renameTestFile = "core-codemods/src/main/java/io/codemodder/codemods/SQLTestMixed.java",
expectingFixesAtLines = {21},
dependencies = {})
class SupportedMixedInjectionTest implements CodemodTestMixin {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.codemodder.codemods;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.util.Scanner;
import java.util.regex.Pattern;

public final class SQLTestMixed {

private Connection conn;

public ResultSet simpleIndirect() throws SQLException {
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
String sql = "SELECT * FROM " + validateTableName(input + "") + " where name=?" ;
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, scanner.nextLine());
return stmt.execute();
}

String validateTableName(final String tablename) {
Pattern regex = Pattern.compile("[a-zA-Z0-9_]+(.[a-zA-Z0-9_]+)?");
if (!regex.matcher(tablename).matches()) {
throw new SecurityException("Supplied table name contains non-alphanumeric characters");
}
return tablename;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.codemodder.codemods;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.util.Scanner;
import java.util.regex.Pattern;

public final class SQLTestMixed {

private Connection conn;

public ResultSet simpleIndirect() throws SQLException {
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
String input2 = scanner.nextLine();
String sql = "SELECT * FROM " + input + " where name='" + input2 + "'" ;
Statement stmt = conn.createStatement();
return stmt.executeQuery(sql);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"paging": {
"pageIndex": 1,
"pageSize": 100,
"total": 1
},
"hotspots": [
{
"key": "AZEIpASKc7kK4RXktkeh",
"component": "pixee_codemodder-java:core-codemods/src/main/java/io/codemodder/codemods/SQLTestMixed.java",
"project": "pixee_codemodder-java",
"securityCategory": "sql-injection",
"vulnerabilityProbability": "HIGH",
"status": "TO_REVIEW",
"line": 21,
"message": "Make sure using a dynamically formatted SQL query is safe here.",
"creationDate": "2024-07-31T13:53:37+0200",
"updateDate": "2024-07-31T13:53:37+0200",
"textRange": {
"startLine": 21,
"endLine": 21,
"startOffset": 33,
"endOffset": 36
},
"flows": [],
"ruleKey": "java:S2077"
}
],
"components": [
{
"organization": "pixee",
"key": "pixee_codemodder-java",
"qualifier": "TRK",
"name": "codemodder-java",
"longName": "codemodder-java",
"pullRequest": "434"
},
{
"organization": "pixee",
"key": "pixee_codemodder-java:core-codemods/src/main/java/io/codemodder/codemods/SQLTestMixed.java",
"qualifier": "FIL",
"name": "SQLTestMixed.java",
"longName": "core-codemods/src/main/java/io/codemodder/codemods/SQLTestMixed.java",
"path": "core-codemods/src/main/java/io/codemodder/codemods/SQLTestMixed.java",
"pullRequest": "434"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.resolution.types.ResolvedType;
import java.util.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -20,7 +26,7 @@ public final class LinearizedStringExpression {
private final Deque<Expression> linearized;

public LinearizedStringExpression(final Expression expression) {
this.resolvedExpressions = new HashMap<>();
this.resolvedExpressions = new IdentityHashMap<>();
this.root = expression;
this.linearized = linearize(expression).collect(Collectors.toCollection(ArrayDeque::new));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
final class DefaultJavaParserSQLInjectionRemediatorStrategy
implements JavaParserSQLInjectionRemediatorStrategy {

private Map<Predicate<MethodCallExpr>, Predicate<MethodCallExpr>> remediationStrategies;
private final Map<Predicate<MethodCallExpr>, Predicate<MethodCallExpr>> remediationStrategies;

/**
* Builds a strategy from a matcher-fixer pair. A matcher is a predicate that matches the call,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,5 @@ <T> CodemodFileScanningResult remediateAll(
/** A default implementation that should be used in all non-test scenarios. */
JavaParserSQLInjectionRemediatorStrategy DEFAULT =
new DefaultJavaParserSQLInjectionRemediatorStrategy(
Map.of(
SQLParameterizer::isSupportedJdbcMethodCall,
SQLParameterizerWithCleanup::checkAndFix,
SQLTableInjectionFilterTransform::matchCall,
SQLTableInjectionFilterTransform::fix));
Map.of(SQLInjectionFixComposer::match, SQLInjectionFixComposer::checkAndFix));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.codemodder.remediation.sqlinjection;

import com.github.javaparser.ast.expr.MethodCallExpr;

andrecsilva marked this conversation as resolved.
Show resolved Hide resolved
/** Composes several transformations related to SQL injections. */
public final class SQLInjectionFixComposer {

private SQLInjectionFixComposer() {}

andrecsilva marked this conversation as resolved.
Show resolved Hide resolved
/**
* Given a {@link MethodCallExpr} related to executing JDBC API SQL queries (i.e.
* prepareStatement(), executeQuery(), etc.), parameterize data injections or add a validation
* step for structural injections.
*/
public static boolean checkAndFix(final MethodCallExpr methodCallExpr) {
// First, check if any data injection fixes apply
var maybeFixed = new SQLParameterizer(methodCallExpr).checkAndFix();
if (maybeFixed.isPresent()) {
// If yes, execute cleanup steps and check if any table injection remains.
SQLParameterizerWithCleanup.cleanup(maybeFixed.get());
SQLTableInjectionFilterTransform.findAndFix(maybeFixed.get());
return true;
// If not, try the table injection only
} else {
return SQLTableInjectionFilterTransform.findAndFix(methodCallExpr);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaDoc

Should this type be an interface and the others be implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit hesitant on that since these keep changing. Notice how the checkAndFix for SQLParameterization is slightly different (returns an Optional), but this difference is necessary for the compostion.

I guess we could have a generic find-and-fix-transform interface if you feel the remediators need a stronger contract.

/**
* Check if the {@link MethodCallExpr} is a JDBC API query method that is a target of a SQL
* injection transformation.
*/
public static boolean match(final MethodCallExpr methodCallExpr) {
return SQLParameterizer.isSupportedJdbcMethodCall(methodCallExpr)
|| SQLTableInjectionFilterTransform.matchCall(methodCallExpr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@ private SQLParameterizerWithCleanup() {}

public static boolean checkAndFix(final MethodCallExpr methodCallExpr) {
var maybeFixed = new SQLParameterizer(methodCallExpr).checkAndFix();
if (maybeFixed.isPresent()) {
// Cleanup
var maybeMethodDecl = methodCallExpr.findAncestor(CallableDeclaration.class);
maybeFixed.ifPresent(call -> cleanup(call));
return maybeFixed.isPresent();
}

public static void cleanup(final MethodCallExpr pstmtCall) {
var maybeMethodDecl = pstmtCall.findAncestor(CallableDeclaration.class);

// Remove concatenation with empty strings e.g "first" + "" -> "first";
maybeMethodDecl.ifPresent(ASTTransforms::removeEmptyStringConcatenation);
// Remove potential unused variables left after transform
maybeMethodDecl.ifPresent(md -> ASTTransforms.removeUnusedLocalVariables(md));
// Remove concatenation with empty strings e.g "first" + "" -> "first";
maybeMethodDecl.ifPresent(ASTTransforms::removeEmptyStringConcatenation);
// Remove potential unused variables left after transform
maybeMethodDecl.ifPresent(md -> ASTTransforms.removeUnusedLocalVariables(md));

// Merge concatenated literals, e.g. "first" + " and second" -> "first and second"
maybeFixed
.flatMap(mce -> mce.getArguments().getFirst())
.ifPresent(ASTTransforms::mergeConcatenatedLiterals);
return true;
}
return false;
// Merge concatenated literals, e.g. "first" + " and second" -> "first and second"
pstmtCall.getArguments().getFirst().ifPresent(ASTTransforms::mergeConcatenatedLiterals);
}
}
Loading