Skip to content

fix(tiering): escape paths in router read_parquet builders (#307) - #501

Merged
xe-nvdk merged 3 commits into
mainfrom
fix/tiering-readparquet-escape
Jun 12, 2026
Merged

xe-nvdk merged 3 commits into
mainfrom
fix/tiering-readparquet-escape

Conversation

@xe-nvdk

@xe-nvdk xe-nvdk commented Jun 12, 2026

Copy link
Copy Markdown
Member

Closes #307.

Summary

The internal/tiering router's read_parquet()-building helpers interpolated storage file paths into DuckDB SQL with '%s' and no single-quote escaping. They now route every path through sqlutil.EscapeStringLiteral (internal/sql/mask.go) — the same chokepoint the live query path already uses via quotePath.

Important framing (found during review)

These helpers — buildReadParquet, BuildReadParquetExpr, BuildMultiTierQuery — are not on the live query path. The only router method production calls is GetGlobPathsForQuery (3 sites in internal/api/query.go), which returns raw glob strings; the query engine then builds tiered read_parquet() calls through its own buildMultiTierReadParquet → quotePath → EscapeStringLiteral, which was already escaped. The patched functions have zero production callers.

So this is defense-in-depth / hardening of currently-unwired builders, not a live-exposure fix. They appear to be built ahead of a tiered-query-routing path that isn't wired yet — escaping them now means a future caller inherits the protection. The release-notes entry and code comments are framed accordingly (no overstated "live SQL injection" claim).

Two reviewers (general correctness + security checklist) independently confirmed: single-quote doubling is the complete and sufficient escape for DuckDB's plain '...' literal in read_parquet() argument position (backslashes are literal; E'...'/dollar-quoting require a sigil the code never emits); no double-escaping (live callers escape downstream independently); no import cycle.

Also addressed

Test plan

  • go build ./cmd/... ./internal/..., go vet ./internal/tiering/, gofmt -l clean
  • go test -race ./internal/tiering/ -count=1 passes
  • New router_test.go pins the escape contract (single/multi-path, quote-bearing, injection-shaped); verified to FAIL without the escape
  • Internal config-matrix + deep review + security-checklist review completed (no Blockers/High code findings; framing corrected per review)

🤖 Generated with Claude Code

…ense-in-depth)

The internal/tiering router's read_parquet()-building helpers
(buildReadParquet, BuildReadParquetExpr, BuildMultiTierQuery)
interpolated file paths into DuckDB SQL without escaping single quotes.
These helpers are not on the live query path — the query engine builds
tiered read_parquet() calls via its own already-escaped quotePath
helper — so this is hardening of currently-unwired builders rather than
a live-exposure fix. Routing paths through sqlutil.EscapeStringLiteral
(the same chokepoint the API layer uses) means any future caller that
wires these up inherits the escaping.

Also documents that BuildMultiTierQuery's whereClause is interpolated
raw — a separate injection sink a future caller must validate — and
adds router_test.go pinning the escape contract (verified to fail
without the fix).

The real-world taint path (unvalidated MQTT topic-mapping names) is
tracked separately in #300.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request hardens the internal/tiering router's read_parquet() query builders by escaping single quotes in interpolated file paths to prevent potential SQL injection. It also adds comprehensive unit tests to verify this escaping behavior and updates the release notes. The review feedback highlights a critical bug in the unwired buildFullPath helper method, which ignores its backendType parameter and always uses the cold backend configuration, potentially leading to incorrect path generation for the hot tier in the future.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/tiering/router.go

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request hardens the tiering router's query-building helpers by escaping single quotes in interpolated file paths to prevent SQL injection, accompanied by new unit tests and release notes. Feedback highlights a critical bug in the buildFullPath helper function, which ignores the backendType parameter and incorrectly applies cold backend configurations to hot tier queries.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/tiering/router.go
…mini round 1)

buildFullPath ignored its backendType parameter and always switched on
coldBackend, so a hot-tier ("local") path would be wrongly prefixed
with the cold s3://|azure:// URI when a cold backend is configured.
Pre-existing; only reachable from the unwired BuildMultiTierQuery, so
no production query is affected (the live tiered path in query.go's
buildMultiTierReadParquet builds hot and cold paths separately and
correctly). Guarded with an explicit backendType=="local" early return
so the builders are correct for any future caller. Test pins the guard.
@xe-nvdk

xe-nvdk commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist Round-1 finding addressed in the latest commit: buildFullPath now returns a local (hot-tier) path unchanged instead of prefixing it with the cold backend URI, with a test pinning the guard.

For the record on exploitability: this is latent, not a production bug. The only caller of buildFullPath is the unwired BuildMultiTierQuery; the live tiered-query path is buildMultiTierReadParquet in internal/api/query.go, which constructs hot (getStoragePath) and cold (storage.GetStoragePath) paths separately and correctly and never calls the router builders. So hot+cold queries work today — the fix prevents the bug from surfacing if these builders are ever wired up. Please take another look.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request hardens the internal/tiering router by escaping single quotes in interpolated DuckDB read_parquet() paths to prevent SQL injection (defense-in-depth, #307) and fixes a bug where hot-tier paths were incorrectly prefixed with cold backend URIs. It also adds comprehensive unit tests to verify these changes. The reviewer recommends adding defensive checks for r.manager == nil in BuildMultiTierQuery and buildFullPath to prevent potential nil pointer dereference panics when the router is initialized without a manager.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/tiering/router.go
Comment thread internal/tiering/router.go
…ound 2)

BuildMultiTierQuery and buildFullPath are exported but unwired; a future
caller (or a test) constructing a Router without a manager would panic
on r.manager deref. Guard both with an early return. No production
effect — NewRouter always supplies a manager.
@xe-nvdk

xe-nvdk commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist Round-2 findings addressed: BuildMultiTierQuery and buildFullPath now guard against a nil manager. Accepted specifically because these are exported, unwired builders intended for a future caller who won't have NewRouter's manager-always-set invariant in hand — a nil-deref panic on a public method is a sharp edge worth removing even though production (via NewRouter) never hits it. Please take another look.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request hardens the tiering router's read_parquet() query builders by escaping single quotes in interpolated file paths to prevent potential SQL injection as a defense-in-depth measure. It also ensures that hot-tier local paths are returned unchanged without being incorrectly prefixed by a cold backend's URI, and adds corresponding unit tests to verify these behaviors. There are no review comments, so I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@xe-nvdk
xe-nvdk merged commit d9d4b04 into main Jun 12, 2026
5 checks passed
@xe-nvdk
xe-nvdk deleted the fix/tiering-readparquet-escape branch June 12, 2026 19:36
xe-nvdk added a commit that referenced this pull request Jun 12, 2026
…502)

BuildReadParquetExpr, BuildMultiTierQuery, buildReadParquet, and
buildFullPath have had zero callers since they were introduced in the
original 2-tier commit (2575746) — the query engine builds tiered
read_parquet() calls inline in internal/api/query.go (buildMultiTier-
ReadParquet → quotePath), never through the router. Confirmed dead by
exhaustive cross-repo reference search: the four functions referenced
only each other and their tests.

PR #501 had just escaped and nil-guarded them as defense-in-depth, but
since they are confirmed unreachable (not pending/warm-tier scaffolding
— warm was removed before the 2-tier code landed), deleting is the
right call: less surface to maintain, and it removes the raw-whereClause
injection sink BuildMultiTierQuery carried. The live router methods
(GetStoragePathsForQuery, GetGlobPathsForQuery, getBackendName) are
untouched. Drops the now-unused sqlutil, storage, and strings imports.

Co-authored-by: Ignacio Van Droogenbroeck <ignacio@vandroogenbroeck.net>
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.

high(tiering): SQL injection in buildReadParquet via path interpolation

1 participant