Skip to main content

Closing the gap: prepared-statement coverage now spans every code path

· 3 min read
DataZoo Team
flAPI Development Team

flAPI v26.05.18 is out. This is a focused follow-up to v26.05.17's security roadmap. After yesterday's release, an internal audit caught that the prepared-statement bind was only wired into the GET executor — POST/PUT/PATCH writes and the Arrow-streaming endpoint still rendered Mustache templates as strings. This release closes that gap and adds the test corpus to prove it.

What changed

Writes now route through the prepared path

executeWrite (which serves POST/PUT/PATCH) now calls the rewriter first, splits the rewritten SQL into statements (quote-aware), distributes the binding plan across statements by counting ? placeholders per statement, and prepares + binds + executes each one. Multi-statement INSERT … ; SELECT … RETURNING templates keep working — each statement is its own prepared statement with the right slice of the binding plan.

Arrow streaming follows the same path

executeQueryRaw (the Arrow-IPC endpoint) now takes the prepared path with the same empty-plan fall-back as the JSON endpoint.

Bind-conversion errors map to HTTP 400

A new flapi::BadRequestError exception class. When a typed parameter can't convert (e.g. id=abc on an int field), the bind layer throws BadRequestError and the request handler returns HTTP 400 with a JSON body — bind-conversion failures are client input errors, not server errors. Prepare/execute failures (genuine server-side problems) still return 500.

Validator hardening

validateDate and validateTime now demand the entire input string be consumed — 2024-03-15' OR 1=1 no longer parses to 2024-03-15 and silently drops the suffix. Same fix as validateInt in v26.05.17.

Coverage matrix

PathDefenseTested
GET (all 9 validator types)Prepared bind✅ 99 corpus payloads
GET with pagination wrapPrepared bind (inner + count + paginated)✅ corpus
POST / PUT / PATCH single-statementPrepared bind✅ 16 write-corpus payloads
POST multi-statement INSERT … ; SELECT … RETURNINGPer-statement binding slice✅ 3 corpus cases
Arrow streaming (executeQueryRaw)Prepared bind✅ via shared rewriter path
countSqlPlaceholders helper (quote / dollar-aware)✅ 6 dedicated unit tests

Tests

  • 586 C++ unit assertions (+6 for countSqlPlaceholders).
  • 483 integration tests (+81 from the corpus extensions). Every classic injection pattern (UNION, OR 1=1, comment-evasion, xkcd 327) either returns zero rows or is rejected at the validator/bind boundary — none execute as SQL, regardless of HTTP method.

Why two releases for the same feature

v26.05.17's CHANGELOG claimed "typed scalar params bound via prepared statements" without qualifying that it was GET-only. The honest answer to the "is it really hard-tested?" question — asked after v26.05.17 shipped — was no, not yet. v26.05.18 is what yes looks like: every user-input-bearing code path, end-to-end test coverage, accurate docs.


Full release notes: CHANGELOG.md · Release v26.05.18 · pip install --upgrade flapi-io

🍪 Cookie Settings