Skip to content

SONARJAVA-5683 S2077 Fix FN on strings built with String.format()/formatted(). #5246

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

Merged
merged 6 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -7,6 +7,7 @@
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.util.Locale;
import org.hibernate.Session;
import javax.persistence.EntityManager;

Expand Down Expand Up @@ -240,3 +241,78 @@ private void foo() {
tmpl.queryForObject(user, String.class); // compliant
}
}

class SQLFormat {
private final Statement stmt;

public SQLFormat(Statement stmt) {
this.stmt = stmt;
}

public void formatInline(String input) throws SQLException {
this.stmt.execute(String.format("SELECT %s", input)); // Noncompliant
}

public void formatInlineConst() throws SQLException {
// Allow strings built with `format` if the arguments are constants.
this.stmt.execute(String.format("SELECT %s", "1"));
}

public void formatVar(String input) throws SQLException {
String query = String.format("SELECT %s", input);
this.stmt.execute(query); // Noncompliant
}

public void formatVarObject(Object input) throws SQLException {
String query = String.format("SELECT %s", input);
this.stmt.execute(query); // Noncompliant
}

public void formatVarConst() throws SQLException {
String query = String.format("SELECT %s", "1");
this.stmt.execute(query);
}

public void formatLocale(Locale locale, String input) throws SQLException {
String query = String.format(locale, "SELECT %s", input);
this.stmt.execute(query); // Noncompliant
}

public void formatLocaleConst(Locale locale) throws SQLException {
String query = String.format(locale, "SELECT %s", "1");
this.stmt.execute(query);
}

public void formatPrimitiveVar(int input) throws SQLException {
String query = String.format("SELECT %s", input);
this.stmt.execute(query);
}

public void formatPrimitiveConst() throws SQLException {
String query = String.format("SELECT %s", 1);
this.stmt.execute(query);
}

public void formatted(String input) throws SQLException {
String query = "SELECT %s".formatted(input);
this.stmt.execute(query); // Noncompliant
}

public void formattedConst() throws SQLException {
String query = "SELECT %s".formatted("1");
this.stmt.execute(query);
}

public void plusAssignment(String input) throws SQLException {
String query = "SELECT";
query += String.format("WHERE col = %c", input);
this.stmt.execute(query); // Noncompliant
}

public void plusAssignmentConst() throws SQLException {
// FP, but probably rare and not worth complicating the code to fix it.
String query = "SELECT";
query += String.format("WHERE col = \"%c\"", "value");
this.stmt.execute(query); // Noncompliant
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
Expand Down Expand Up @@ -112,6 +113,14 @@ public class SQLInjectionCheck extends IssuableSubscriptionVisitor {
.withAnyParameters()
.build());

private static final String JAVA_LANG_STRING = "java.lang.String";

private static final MethodMatchers FORMAT_METHODS = MethodMatchers.create()
.ofTypes(JAVA_LANG_STRING)
.names("format", "formatted")
.withAnyParameters()
.build();

private static final String MAIN_MESSAGE = "Make sure using a dynamically formatted SQL query is safe here.";

@Override
Expand All @@ -123,20 +132,20 @@ public List<Tree.Kind> nodesToVisit() {
public void visitNode(Tree tree) {
if (anyMatch(tree)) {
Optional<ExpressionTree> sqlStringArg = arguments(tree)
.filter(arg -> arg.symbolType().is("java.lang.String"))
.filter(arg -> arg.symbolType().is(JAVA_LANG_STRING))
.findFirst();

if (sqlStringArg.isPresent()) {
ExpressionTree sqlArg = sqlStringArg.get();
if (isDynamicConcatenation(sqlArg)) {
if (isDynamicString(sqlArg)) {
reportIssue(sqlArg, MAIN_MESSAGE);
} else if (sqlArg.is(Tree.Kind.IDENTIFIER)) {
IdentifierTree identifierTree = (IdentifierTree) sqlArg;
Symbol symbol = identifierTree.symbol();
ExpressionTree initializerOrExpression = getInitializerOrExpression(symbol.declaration());
List<AssignmentExpressionTree> reassignments = getReassignments(symbol.owner().declaration(), symbol.usages());

if ((initializerOrExpression != null && isDynamicConcatenation(initializerOrExpression)) ||
if ((initializerOrExpression != null && isDynamicString(initializerOrExpression)) ||
reassignments.stream().anyMatch(SQLInjectionCheck::isDynamicPlusAssignment)) {
reportIssue(sqlArg, MAIN_MESSAGE, secondaryLocations(initializerOrExpression, reassignments, identifierTree.name()), null);
}
Expand Down Expand Up @@ -197,7 +206,35 @@ private static boolean isDynamicPlusAssignment(ExpressionTree arg) {
return arg.is(Tree.Kind.PLUS_ASSIGNMENT) && !((AssignmentExpressionTree) arg).expression().asConstant().isPresent();
}

private static boolean isDynamicString(ExpressionTree arg) {
return isDynamicConcatenation(arg) || isDynamicFormat(arg);
}

private static boolean isDynamicConcatenation(ExpressionTree arg) {
return arg.is(Tree.Kind.PLUS) && !arg.asConstant().isPresent();
}

private static boolean isDynamicFormat(Tree tree) {
return tree instanceof MethodInvocationTree mit
&& FORMAT_METHODS.matches(mit)
&& hasDynamicStringParameters(mit);
}

/**
* Checks if parameters to format/formatted are dynamic variables susceptible to SQL injection.
*/
private static boolean hasDynamicStringParameters(MethodInvocationTree mit) {
boolean firstArg = true;
for (ExpressionTree arg: mit.arguments()) {
Type type = arg.symbolType();
// `format` has a variant with Locale as the first argument - we do not need to check that parameter.
boolean isFirstLocaleArgument = firstArg && !type.isUnknown() && type.is("java.util.Locale");
Copy link
Member

Choose a reason for hiding this comment

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

&& !type.isUnknown() seems useless, remove or invert the logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved and tested.

// Primitives will not lead to SQL injection, so the code is compliant.
if (!isFirstLocaleArgument && !type.isPrimitive() && arg.asConstant().isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

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

We could also use isPrimitiveWrapper

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

return true;
}
firstArg = false;
}
return false;
}
}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy