diff --git a/java/ql/lib/change-notes/2026-08-04-spring-r2dbc-sql-injection.md b/java/ql/lib/change-notes/2026-08-04-spring-r2dbc-sql-injection.md new file mode 100644 index 000000000000..a8d4de85e4cb --- /dev/null +++ b/java/ql/lib/change-notes/2026-08-04-spring-r2dbc-sql-injection.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added SQL injection sink models for Spring R2DBC `DatabaseClient` and the R2DBC SPI. diff --git a/java/ql/lib/ext/io.r2dbc.spi.model.yml b/java/ql/lib/ext/io.r2dbc.spi.model.yml new file mode 100644 index 000000000000..7edfcdc36643 --- /dev/null +++ b/java/ql/lib/ext/io.r2dbc.spi.model.yml @@ -0,0 +1,11 @@ +extensions: + - addsTo: + pack: codeql/java-all + extensible: sinkModel + data: + - ["io.r2dbc.spi", "Connection", True, "createStatement", "(String)", "", "Argument[0]", "sql-injection", "manual"] + - ["io.r2dbc.spi", "Batch", True, "add", "(String)", "", "Argument[0]", "sql-injection", "manual"] + - ["io.r2dbc.spi", "Connection", True, "createSavepoint", "(String)", "", "Argument[0]", "sql-injection", "manual"] + - ["io.r2dbc.spi", "Connection", True, "releaseSavepoint", "(String)", "", "Argument[0]", "sql-injection", "manual"] + - ["io.r2dbc.spi", "Connection", True, "rollbackTransactionToSavepoint", "(String)", "", "Argument[0]", "sql-injection", "manual"] + - ["io.r2dbc.spi", "Statement", True, "returnGeneratedValues", "(String[])", "", "Argument[0]", "sql-injection", "manual"] diff --git a/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml b/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml new file mode 100644 index 000000000000..d388e72fdcc9 --- /dev/null +++ b/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml @@ -0,0 +1,27 @@ +extensions: + - addsTo: + pack: codeql/java-all + extensible: sinkModel + data: + - ["org.springframework.r2dbc.core", "DatabaseClient", True, "sql", "(String)", "", "Argument[0]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "fetch", "", "", "Argument[this]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "then", "", "", "Argument[this]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "map", "(java.util.function.Function)", "", "Argument[this]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "map", "(java.util.function.BiFunction)", "", "Argument[this]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "mapValue", "(Class)", "", "Argument[this]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "mapProperties", "(Class)", "", "Argument[this]", "sql-injection", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "flatMap", "(java.util.function.Function)", "", "Argument[this]", "sql-injection", "manual"] + - addsTo: + pack: codeql/java-all + extensible: summaryModel + data: + - ["org.springframework.r2dbc.core", "DatabaseClient", True, "sql", "(java.util.function.Supplier)", "", "Argument[0].ReturnValue", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bind", "(int,Object)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bind", "(String,Object)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bindNull", "(int,Class)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bindNull", "(String,Class)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bindValues", "(List)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bindValues", "(Map)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "bindProperties", "(Object)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "filter", "(java.util.function.Function)", "", "Argument[this]", "ReturnValue", "taint", "manual"] + - ["org.springframework.r2dbc.core", "DatabaseClient$GenericExecuteSpec", True, "filter", "(org.springframework.r2dbc.core.StatementFilterFunction)", "", "Argument[this]", "ReturnValue", "taint", "manual"] diff --git a/java/ql/test/query-tests/security/CWE-089/semmle/examples/SpringJdbc.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/SpringJdbc.java index 2772ce95536a..656e9c69abf4 100644 --- a/java/ql/test/query-tests/security/CWE-089/semmle/examples/SpringJdbc.java +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/SpringJdbc.java @@ -1,5 +1,8 @@ import java.sql.ResultSet; import java.util.Map; +import java.util.function.Supplier; +import io.r2dbc.spi.Batch; +import io.r2dbc.spi.Connection; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; @@ -13,6 +16,7 @@ import org.springframework.jdbc.object.SqlFunction; import org.springframework.jdbc.object.SqlUpdate; import org.springframework.jdbc.object.UpdatableSqlQuery; +import org.springframework.r2dbc.core.DatabaseClient; public class SpringJdbc { @@ -80,4 +84,33 @@ public static void test(JdbcTemplate template, NamedParameterJdbcOperations name namedParamTemplate.update(source(), null, null, null); // $ sqlInjection } + public static void testR2dbc(DatabaseClient client, Connection connection, Batch batch) { + client.sql(source()); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).fetch(); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).then(); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).map(row -> row); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).map((row, metadata) -> row); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).flatMap(row -> row); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).mapValue(String.class); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source).mapProperties(String.class); // $ sqlInjection + client.sql((Supplier) SpringJdbc::source) + .bind(0, "value") + .bind("name", "value") + .bindNull(0, String.class) + .bindNull("name", String.class) + .bindValues(java.util.List.of("value")) + .bindValues(java.util.Map.of("name", "value")) + .bindProperties(new Object()) + .filter(statement -> statement) + .filter((org.springframework.r2dbc.core.StatementFilterFunction) null) // $ sqlInjection + .fetch(); + connection.createStatement(source()); // $ sqlInjection + batch.add(source()); // $ sqlInjection + connection.createSavepoint(source()); // $ sqlInjection + connection.releaseSavepoint(source()); // $ sqlInjection + connection.rollbackTransactionToSavepoint(source()); // $ sqlInjection + connection.createStatement("INSERT INTO test VALUES (1)") + .returnGeneratedValues(source()); // $ sqlInjection + } + } \ No newline at end of file diff --git a/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Batch.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Batch.java new file mode 100644 index 000000000000..dda062e2362f --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Batch.java @@ -0,0 +1,5 @@ +package io.r2dbc.spi; + +public interface Batch { + Batch add(String sql); +} diff --git a/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Connection.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Connection.java new file mode 100644 index 000000000000..0269be9693c5 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Connection.java @@ -0,0 +1,11 @@ +package io.r2dbc.spi; + +public interface Connection { + Statement createStatement(String sql); + + void createSavepoint(String name); + + void releaseSavepoint(String name); + + void rollbackTransactionToSavepoint(String name); +} diff --git a/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Statement.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Statement.java new file mode 100644 index 000000000000..216629d0dad3 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Statement.java @@ -0,0 +1,5 @@ +package io.r2dbc.spi; + +public interface Statement { + Statement returnGeneratedValues(String... columns); +} diff --git a/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/DatabaseClient.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/DatabaseClient.java new file mode 100644 index 000000000000..f36da8510dc5 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/DatabaseClient.java @@ -0,0 +1,47 @@ +package org.springframework.r2dbc.core; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface DatabaseClient { + GenericExecuteSpec sql(String sql); + + GenericExecuteSpec sql(Supplier sqlSupplier); + + interface GenericExecuteSpec { + GenericExecuteSpec bind(int index, Object value); + + GenericExecuteSpec bind(String name, Object value); + + GenericExecuteSpec bindNull(int index, Class type); + + GenericExecuteSpec bindNull(String name, Class type); + + GenericExecuteSpec bindValues(List values); + + GenericExecuteSpec bindValues(Map values); + + GenericExecuteSpec bindProperties(Object source); + + GenericExecuteSpec filter(Function filterFunction); + + GenericExecuteSpec filter(StatementFilterFunction filterFunction); + + Object fetch(); + + Object then(); + + Object map(Function mappingFunction); + + Object map(BiFunction mappingFunction); + + Object mapValue(Class mappedClass); + + Object mapProperties(Class mappedClass); + + Object flatMap(Function mappingFunction); + } +} diff --git a/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/StatementFilterFunction.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/StatementFilterFunction.java new file mode 100644 index 000000000000..871c6dae9670 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/StatementFilterFunction.java @@ -0,0 +1,4 @@ +package org.springframework.r2dbc.core; + +public interface StatementFilterFunction { +}