Skip to content

Fix false negative: full SQL-injection sink coverage for Spring R2DBC + io.r2dbc.spi - #216

Merged
felickz merged 1 commit into
mainfrom
fix/r2dbc-sql-injection-coverage
Aug 5, 2026
Merged

Fix false negative: full SQL-injection sink coverage for Spring R2DBC + io.r2dbc.spi#216
felickz merged 1 commit into
mainfrom
fix/r2dbc-sql-injection-coverage

Conversation

@felickz

@felickz felickz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a false-negative gap in SQL-injection detection for Spring R2DBC's deferred DatabaseClient.sql(Supplier<String>) pattern. This PR goes beyond the minimal fix to give full, TDD-validated coverage of both affected data extensions, per a deep-dive assessment of the whole spring-r2dbc / io.r2dbc.spi API surface.

java/ext/manual/org.springframework.r2dbc.model.yml

Previously only DatabaseClient.GenericExecuteSpec.fetch() was modeled as a sink. That missed:

  • Terminal methods: then(), map(Function), map(BiFunction), mapValue(Class), mapProperties(Class), flatMap(Function)6 new sinkModel rows
  • Fluent methods that must carry taint through the chain: bind(int/String,Object), bindNull(int/String,Class), bindValues(List/Map), bindProperties(Object), filter(Function/StatementFilterFunction)9 new summaryModel rows

Every one of the 16 total rows (7 sink + 9 summary) has a dedicated, empirically-verified test — not architectural reasoning by analogy.

java/ext/manual/io.r2dbc.spi.model.yml

Full audit of the driver-agnostic R2DBC SPI (Connection, Statement, Batch) found 4 more unmodeled sinks:

  • Connection.createSavepoint(String)
  • Connection.releaseSavepoint(String)
  • Connection.rollbackTransactionToSavepoint(String)
  • Statement.returnGeneratedValues(String...)

Each takes a raw identifier (savepoint name / column name) that real drivers splice unescaped into the executed SQL text, since the wire protocol has no parameter placeholder for identifiers (only for literal values). Verified against r2dbc-postgresql 1.0.2.RELEASE source:

  • PostgresqlConnection.createSavepoint/releaseSavepoint/rollbackTransactionToSavepointString.format("SAVEPOINT %s", name) (and RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT), zero escaping
  • GeneratedValuesUtils.augment()String.format("%s RETURNING %s", sql, String.join(", ", columns)), zero escaping

With Connection/Statement/Batch now fully enumerated against real interface source, every String/String[]-carrying method that can influence executed SQL text is modeled.

Methodology

For every row (existing and new): write a failing test exercising the exact real method signature → add the model row → confirm the test passes → confirm no regression in the broader suite. This caught two real bugs along the way:

  1. A test-stub structural bug — StatementFilterFunction needed to be a top-level type (matching real Spring), not nested inside DatabaseClient, or the model's fully-qualified signature string silently fails to match.
  2. An accidental .expected corruption from copying tool-generated line-number prefixes as if they were file content.

Testing

  • codeql test run test/security/CWE-089/spring-r2dbc passes cleanly (21 detected flows, all expected).
  • Broader codeql test run test/security/CWE-089 shows only pre-existing, unrelated MyBatis failures caused by a missing external stub checkout path in this environment — not caused by this change.

Scope notes

  • Scoped to the sql(Supplier<String>) pattern on GenericExecuteSpec as of current spring-framework main, and the io.r2dbc.spi SPI as of current r2dbc-spi main. Other deferred-SQL entry points elsewhere in spring-data-r2dbc (e.g. R2dbcEntityTemplate) were not audited in this pass.
  • IsolationLevel.valueOf(String sql) technically stores a raw SQL string later spliced into SET TRANSACTION ISOLATION LEVEL <asSql()>, but real code always uses the 4 static constants rather than a dynamic string — judged not worth modeling given the added complexity for near-zero real-world occurrence.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…pi coverage

Fixes the false-negative reported in github/field-security-codeql#231 for
Spring R2DBC's DatabaseClient.sql(Supplier<String>) pattern, and extends the
fix to full, TDD-validated coverage of both affected data extensions.

org.springframework.r2dbc.model.yml:
- Previously only DatabaseClient.GenericExecuteSpec.fetch() was modeled as a
  sink, missing then()/map()/mapValue()/mapProperties()/flatMap() terminal
  methods and the bind*/filter fluent methods that carry the tainted SQL
  through the chain.
- Added 6 sinkModel rows (then, map(Function), map(BiFunction), mapValue,
  mapProperties, flatMap) and 9 summaryModel rows (bind x2, bindNull x2,
  bindValues x2, bindProperties, filter x2) so every method on
  GenericExecuteSpec that can reach or carry the deferred SQL is covered.

io.r2dbc.spi.model.yml:
- Added 4 new sinkModel rows found during a full audit of the driver-agnostic
  R2DBC SPI: Connection.createSavepoint(String), releaseSavepoint(String),
  rollbackTransactionToSavepoint(String), and
  Statement.returnGeneratedValues(String...). Each takes a raw
  identifier/column name that real drivers (verified against
  r2dbc-postgresql 1.0.2.RELEASE) splice unescaped into the executed SQL
  text (e.g. String.format("SAVEPOINT %s", name)), since the wire protocol
  has no parameter placeholder for identifiers.

Every one of the 16 Spring R2DBC rows and 6 io.r2dbc.spi rows now has a
dedicated, empirically-verified test proving it fires - not just
architectural reasoning by analogy. Two real bugs were caught and fixed
during this process: a corrupted .expected file (accidental copy of the
view tool's line-number prefixes) and a test-stub structural bug
(StatementFilterFunction needing to be a top-level type, not nested inside
DatabaseClient, to match the real Spring API and the model's fully-qualified
signature string).

Full CWE-089/spring-r2dbc test suite passes cleanly; broader CWE-089 suite
run shows only pre-existing, unrelated MyBatis failures caused by a missing
external stub checkout path in this environment.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f02ef23b-e35d-4536-bd58-f2fbc2976ba9
Copilot AI lite review requested due to automatic review settings August 4, 2026 22:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a SQL-injection false negative in the Java security modeling by expanding sink and taint-propagation coverage for Spring R2DBC’s deferred DatabaseClient.sql(Supplier<String>) API and by adding additional SQL-identifier-related sinks in the low-level io.r2dbc.spi interfaces. The changes are validated via expanded, signature-specific CWE-089 tests and updated expected results.

Changes:

  • Add execution-stage sink coverage for additional DatabaseClient.GenericExecuteSpec terminal methods (then, map*, flatMap) when SQL is deferred via sql(Supplier<String>).
  • Add fluent-chain taint preservation for GenericExecuteSpec binding/filtering methods (bind*, bindValues*, bindProperties, filter*) so deferred-SQL taint is not dropped before reaching execution-stage sinks.
  • Model additional io.r2dbc.spi sinks for identifier-splicing APIs (Connection.*Savepoint(String), Statement.returnGeneratedValues(String...)) and extend the test suite accordingly.
Show a summary per file
File Description
java/ext/manual/org.springframework.r2dbc.model.yml Expands Spring R2DBC sink + summary models to cover deferred-SQL execution-stage methods and preserve taint through fluent bind/filter chains.
java/ext/manual/io.r2dbc.spi.model.yml Adds additional R2DBC SPI sinks for savepoint names and RETURNING column identifiers.
java/test/security/CWE-089/spring-r2dbc/org/springframework/r2dbc/core/DatabaseClient.java Extends the Spring DatabaseClient test stub to include the newly modeled methods/signatures used by tests.
java/test/security/CWE-089/spring-r2dbc/org/springframework/r2dbc/core/StatementFilterFunction.java Adds a top-level stub type needed to exercise filter(StatementFilterFunction) signature matching.
java/test/security/CWE-089/spring-r2dbc/io/r2dbc/spi/Statement.java Extends the R2DBC SPI test stub with returnGeneratedValues(String...).
java/test/security/CWE-089/spring-r2dbc/io/r2dbc/spi/Connection.java Extends the R2DBC SPI test stub with savepoint methods that are now modeled as sinks.
java/test/security/CWE-089/spring-r2dbc/com/example/DeferredQueryHandler.java Adds targeted test cases covering each new Spring R2DBC sink/summary row for deferred SQL.
java/test/security/CWE-089/spring-r2dbc/com/example/RawR2dbcHandler.java Adds targeted test cases for the newly modeled R2DBC SPI sinks (savepoints and generated values).
java/test/security/CWE-089/spring-r2dbc/DatabaseClientSqlInjection.expected Updates expected flows to reflect the newly modeled sinks/paths.

Review details

  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Lite

@felickz
felickz merged commit 3d9904a into main Aug 5, 2026
24 checks passed
@felickz
felickz deleted the fix/r2dbc-sql-injection-coverage branch August 5, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants