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 3 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,63 @@ 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 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 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 @@ -112,6 +112,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 +131,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 +205,26 @@ 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);
}

private static boolean hasDynamicStringParameters(MethodInvocationTree mit) {
for (ExpressionTree arg: mit.arguments()) {
if (arg.symbolType().is(JAVA_LANG_STRING) && arg.asConstant().isEmpty()) {
return true;
}
}
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is it intentional not to support String.format(String format, Object... args) when args is not a String?

For example, if the argument is a int. Why does the rule not support the following case?

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

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

Could be done with this kind of logic:

Suggested change
private static boolean hasDynamicStringParameters(MethodInvocationTree mit) {
for (ExpressionTree arg: mit.arguments()) {
if (arg.symbolType().is(JAVA_LANG_STRING) && arg.asConstant().isEmpty()) {
return true;
}
}
return false;
}
private static boolean hasDynamicStringParameters(MethodInvocationTree mit) {
boolean firstArg = true;
for (ExpressionTree arg: mit.arguments()) {
Type type = arg.symbolType();
boolean notTheFirstLocaleArgument = !firstArg || !type.isUnknown() || !type.is("java.util.Locale");
if (notTheFirstLocaleArgument && arg.asConstant().isEmpty()) {
return true;
}
firstArg = false;
}
return false;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to change it due to a failing test and then expanded it to exclude primitive arguments as well. Please see code comments for details.

}
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