DatabaseClient binds values through the driver so caller-supplied identifiers remain data rather than becoming executable SQL text.
Spring R2DBC named binds: keep a receipt identifier out of SQL syntax
Bind values at the query boundary
The checked query asks for an SKU containing quote characters and an OR expression. With :sku bound as a value, it finds no row and leaves the real SKU-47 balance at 19. String concatenation would change the SQL text and create an injection path. Bind every input value, including tenant, ID, version, and amounts. Column or table names are different: they cannot be made safe by value binding; choose those from server-owned allowlists.
The fixture uses an embedded H2 R2DBC driver. It proves that this query treats the hostile string as data. It does not establish that every SQL builder elsewhere is safe or that a production driver has identical query plans. The JDBC tenant predicate shows the same ownership requirement through a blocking API.
Do not over-read an empty result
The query uses one(), which yields no value when no row matches. The service layer still needs an explicit absent-row decision. A missing SKU, wrong tenant, and unavailable database should not all become the same successful empty response. Keep failure classification near the API boundary and do not log untrusted raw SQL fragments.
Checked excerpt
Mono<Integer> found = database.sql(
"select available from stock_balance where sku = :sku")
.bind("sku", "SKU-47' or '1'='1")
.map((row, metadata) -> row.get("available", Integer.class))
.one();
StepVerifier.create(found).verifyComplete();Verification boundary
The Java 21 / Spring Boot 4 source kit checks boundIdentifierIsDataRatherThanSql in the isolated reactive-r2dbc Maven project.
Cost and limits
Binding has a small driver cost but avoids reparsing attacker-controlled SQL text and supports plan reuse where the database permits it. The real cost is the indexed lookup and connection acquisition, not the string validation example.
Common Mistakes
- Do not concatenate caller input into SQL text.
- Do not expect bind markers to parameterize table or column names.
- Do not confuse an empty result with a database error.
Read next
Spring R2DBC subscription boundary: constructing SQL does not execute it, Spring JdbcTemplate tenant predicates: put ownership in the SQL query, Spring R2DBC conditional update: test tenant, version and stock floor in one statement, Spring GraphQL tenant reads: scope every resolver before returning a record, Spring JdbcTemplate: bound values and visible database constraints.
