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