Fix false negative: full SQL-injection sink coverage for Spring R2DBC + io.r2dbc.spi - #216
Merged
Merged
Conversation
…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
Contributor
There was a problem hiding this comment.
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.GenericExecuteSpecterminal methods (then,map*,flatMap) when SQL is deferred viasql(Supplier<String>). - Add fluent-chain taint preservation for
GenericExecuteSpecbinding/filtering methods (bind*,bindValues*,bindProperties,filter*) so deferred-SQL taint is not dropped before reaching execution-stage sinks. - Model additional
io.r2dbc.spisinks 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 wholespring-r2dbc/io.r2dbc.spiAPI surface.java/ext/manual/org.springframework.r2dbc.model.ymlPreviously only
DatabaseClient.GenericExecuteSpec.fetch()was modeled as a sink. That missed:then(),map(Function),map(BiFunction),mapValue(Class),mapProperties(Class),flatMap(Function)— 6 new sinkModel rowsbind(int/String,Object),bindNull(int/String,Class),bindValues(List/Map),bindProperties(Object),filter(Function/StatementFilterFunction)— 9 new summaryModel rowsEvery 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.ymlFull 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-postgresql1.0.2.RELEASE source:PostgresqlConnection.createSavepoint/releaseSavepoint/rollbackTransactionToSavepoint→String.format("SAVEPOINT %s", name)(andRELEASE SAVEPOINT/ROLLBACK TO SAVEPOINT), zero escapingGeneratedValuesUtils.augment()→String.format("%s RETURNING %s", sql, String.join(", ", columns)), zero escapingWith
Connection/Statement/Batchnow 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:
StatementFilterFunctionneeded to be a top-level type (matching real Spring), not nested insideDatabaseClient, or the model's fully-qualified signature string silently fails to match..expectedcorruption from copying tool-generated line-number prefixes as if they were file content.Testing
codeql test run test/security/CWE-089/spring-r2dbcpasses cleanly (21 detected flows, all expected).codeql test run test/security/CWE-089shows only pre-existing, unrelated MyBatis failures caused by a missing external stub checkout path in this environment — not caused by this change.Scope notes
sql(Supplier<String>)pattern onGenericExecuteSpecas of currentspring-frameworkmain, and theio.r2dbc.spiSPI as of currentr2dbc-spimain. Other deferred-SQL entry points elsewhere inspring-data-r2dbc(e.g.R2dbcEntityTemplate) were not audited in this pass.IsolationLevel.valueOf(String sql)technically stores a raw SQL string later spliced intoSET 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