From 2e4a1745b602f6f252ceca6914fb8935e0e4dee2 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:19:08 -0400 Subject: [PATCH 1/3] Java: model R2DBC SQL injection sinks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fb2a5047-e061-44ba-b6df-198390190733 --- .../2026-08-04-spring-r2dbc-sql-injection.md | 4 ++++ java/ql/lib/ext/io.r2dbc.spi.model.yml | 7 +++++++ .../ext/org.springframework.r2dbc.core.model.yml | 12 ++++++++++++ .../CWE-089/semmle/examples/SpringJdbc.java | 11 +++++++++++ .../CWE-089/semmle/examples/io/r2dbc/spi/Batch.java | 5 +++++ .../semmle/examples/io/r2dbc/spi/Connection.java | 5 +++++ .../semmle/examples/io/r2dbc/spi/Statement.java | 4 ++++ .../springframework/r2dbc/core/DatabaseClient.java | 13 +++++++++++++ 8 files changed, 61 insertions(+) create mode 100644 java/ql/lib/change-notes/2026-08-04-spring-r2dbc-sql-injection.md create mode 100644 java/ql/lib/ext/io.r2dbc.spi.model.yml create mode 100644 java/ql/lib/ext/org.springframework.r2dbc.core.model.yml create mode 100644 java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Batch.java create mode 100644 java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Connection.java create mode 100644 java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Statement.java create mode 100644 java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/DatabaseClient.java 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..9515bee9fe57 --- /dev/null +++ b/java/ql/lib/ext/io.r2dbc.spi.model.yml @@ -0,0 +1,7 @@ +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"] 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..5b186d3efe5b --- /dev/null +++ b/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml @@ -0,0 +1,12 @@ +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"] + - 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"] 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..627469caefb5 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,11 @@ 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 + connection.createStatement(source()); // $ sqlInjection + batch.add(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..76a2c0076236 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Connection.java @@ -0,0 +1,5 @@ +package io.r2dbc.spi; + +public interface Connection { + Statement createStatement(String sql); +} 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..6080305243b7 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Statement.java @@ -0,0 +1,4 @@ +package io.r2dbc.spi; + +public interface Statement { +} 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..a2c5f3ea781e --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/DatabaseClient.java @@ -0,0 +1,13 @@ +package org.springframework.r2dbc.core; + +import java.util.function.Supplier; + +public interface DatabaseClient { + GenericExecuteSpec sql(String sql); + + GenericExecuteSpec sql(Supplier sqlSupplier); + + interface GenericExecuteSpec { + Object fetch(); + } +} From e079fdf269431eb8fcac4a99192bd166eb099fdc Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:34:02 -0400 Subject: [PATCH 2/3] Java: cover R2DBC fluent execution paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fb2a5047-e061-44ba-b6df-198390190733 --- .../org.springframework.r2dbc.core.model.yml | 15 ++++++++ .../CWE-089/semmle/examples/SpringJdbc.java | 17 ++++++++++ .../r2dbc/core/DatabaseClient.java | 34 +++++++++++++++++++ .../r2dbc/core/StatementFilterFunction.java | 4 +++ 4 files changed, 70 insertions(+) create mode 100644 java/ql/test/query-tests/security/CWE-089/semmle/examples/org/springframework/r2dbc/core/StatementFilterFunction.java diff --git a/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml b/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml index 5b186d3efe5b..d388e72fdcc9 100644 --- a/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml +++ b/java/ql/lib/ext/org.springframework.r2dbc.core.model.yml @@ -5,8 +5,23 @@ extensions: 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 627469caefb5..5646c9da3ef1 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 @@ -87,6 +87,23 @@ public static void test(JdbcTemplate template, NamedParameterJdbcOperations name 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 } 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 index a2c5f3ea781e..f36da8510dc5 100644 --- 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 @@ -1,5 +1,9 @@ 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 { @@ -8,6 +12,36 @@ public interface DatabaseClient { 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 { +} From 90b78c980a7ccf1caf0bfa120b1de3d053ea0d3c Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:45:52 -0400 Subject: [PATCH 3/3] Java: add R2DBC SPI SQL injection sinks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fb2a5047-e061-44ba-b6df-198390190733 --- java/ql/lib/ext/io.r2dbc.spi.model.yml | 4 ++++ .../security/CWE-089/semmle/examples/SpringJdbc.java | 5 +++++ .../CWE-089/semmle/examples/io/r2dbc/spi/Connection.java | 6 ++++++ .../CWE-089/semmle/examples/io/r2dbc/spi/Statement.java | 1 + 4 files changed, 16 insertions(+) diff --git a/java/ql/lib/ext/io.r2dbc.spi.model.yml b/java/ql/lib/ext/io.r2dbc.spi.model.yml index 9515bee9fe57..7edfcdc36643 100644 --- a/java/ql/lib/ext/io.r2dbc.spi.model.yml +++ b/java/ql/lib/ext/io.r2dbc.spi.model.yml @@ -5,3 +5,7 @@ extensions: 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/test/query-tests/security/CWE-089/semmle/examples/SpringJdbc.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/SpringJdbc.java index 5646c9da3ef1..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 @@ -106,6 +106,11 @@ public static void testR2dbc(DatabaseClient client, Connection connection, Batch .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/Connection.java b/java/ql/test/query-tests/security/CWE-089/semmle/examples/io/r2dbc/spi/Connection.java index 76a2c0076236..0269be9693c5 100644 --- 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 @@ -2,4 +2,10 @@ 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 index 6080305243b7..216629d0dad3 100644 --- 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 @@ -1,4 +1,5 @@ package io.r2dbc.spi; public interface Statement { + Statement returnGeneratedValues(String... columns); }