Skip to content

fix(security): authorize the invoked operation against the authenticated principal - #2217

Open
cb1kenobi wants to merge 2 commits into
mainfrom
fix/choose-operation-authz
Open

fix(security): authorize the invoked operation against the authenticated principal#2217
cb1kenobi wants to merge 2 commits into
mainfrom
fix/choose-operation-authz

Conversation

@cb1kenobi

@cb1kenobi cb1kenobi commented Aug 19, 2026

Copy link
Copy Markdown
Member

Fixes operation authorization in chooseOperation: authorize the invoked operation against the authenticated principal, and stop a caller-supplied search_operation from standing in as the permission subject.

What this fixes

verifyPerms reads both halves of the permission question off the object it's handed — the principal from hdb_user and the tables from schema/database/table/records. chooseOperation used to hand it json.search_operation, a caller-supplied field, making both halves body-controlled. Four issues, each with regression cover in integrationTests/security/choose-operation-authz.test.ts:

  1. Cross-privilege redirectverifyPerms ran against json.search_operation ?? json for every operation, but the handler runs against top-level json and only dataLayer/export.ts consumes search_operation. A non-super user could send a privileged top-level operation with a benign search_operation and get authorized against the benign tables. Now search_operation is the permission subject only for export_local/export_to_s3; every other op is checked against top-level json.
  2. Principal smuggling — the nested principal was backfilled (if (!hdb_user)), honoring a body-supplied one. Now hdb_user is overwritten from the authenticated top-level principal unconditionally.
  3. parsed_sql_object smuggling — the export worker re-reads parsed_sql_object (carrying permissions_checked) off the nested object, so a body-supplied one would execute an AST no check ever saw. It's now deleted at dispatch, forcing a re-parse + re-check in the worker.
  4. Silent SQL denialprocessAST's guard tested permissionsCheck.length > 0, but a denial is a PermissionResponseObject with no .length, so undefined > 0 was always false and denials executed. Fixed to if (permissionsCheck).

Non-object search_operation on an export op is now a 400 (was a wrapped 500), and the SQL AST check runs additively after verifyPerms rather than as an exclusive branch.

For the reviewer

  • server/serverHelpers/serverUtilities.ts (chooseOperation) is the core. The load-bearing invariant: the object passed to verifyPerms must be the one the handler actually operates on. DESIGN.md (new chooseOperation section) documents the three rules and why each is required.
  • export_local table check is currently unreachable — both export ops are requires_su and every path returns before the nested table check (super_user early-returns; a role granted the op via operations returns at gate 2; anything else is refused earlier). So the nested-table substitution is inert today. This is intentional for this patch.

Behavior change (release note)

Non-super users can no longer invoke export_local / export_to_s3 via a SQL search_operation. These operations are requires_su, and NoSQL export already enforced that ("Operation 'export_local' is restricted to 'super_user' roles"). SQL-based export previously took the SQL-only branch and skipped verifyPerms, so a non-super user could invoke the privileged export by wrapping it in a SQL search — governed only by table read perms. Routing export through verifyPerms closes that bypass and brings SQL export to parity with NoSQL export. Any non-super role that relied on SQL-export-without-export_local will now receive a 403. Three northwind export tests that encoded the old bypass were updated to assert the requires_su denial (they now mirror the existing NoSQL export case; they could be consolidated).

Deferred gaps (tracked, not fixed here)

Two pre-existing authorization gaps are left open deliberately — closing either changes authorization outcomes for existing role configurations, so each needs its own change + release note. Both are documented in DESIGN.md and pinned by regression tests so there's a test to flip:

Verification

  • integrationTests/security/choose-operation-authz.test.ts — pins the denial shapes and the current boundaries (principal override, nested-shape 400, forged-AST rejection, and the two deferred-gap boundaries NESTED-NOSQL / gate-1).
  • Deep-review, multipass (2 independent runs × auth + SQL/AST lenses). Confirmed the fix closes the cross-privilege redirect, principal smuggling, AST smuggling, and the processAST denial-drop, with no other .length-style denial-drop anywhere in the tree. The multipass independently rediscovered the two deferred gaps above (now ticketed). One low cleanup nit surfaced: serverUtilities.ts:298 sets a top-level parsed_sql_object that's inert for the export path.

Review coverage

  • Deep-review, multipass (Claude — Harper-domain + SQL/AST lenses, 2 independent runs): confirmed the fix closes the cross-privilege redirect, principal smuggling, AST smuggling, and the processAST denial-drop; no other .length-style denial-drop in the tree.
  • Cross-model, Codex (graded, opposite-family): verdict COMMENTS, no production blockers. Two findings, both in the test file (below).
  • Degraded legs (noted for honesty): Gemini agy failed (stochastic print-mode hang; CLI retries + 2 manual attempts all timed out) — no perf/maintainability lens this run; cursor-composer not authenticated locally; the CLI's Claude domain-adjudicator leg couldn't run (claude not on PATH) — covered instead by the separate multipass deep-review above.

Unresolved review findings (test-only, non-blocking)

  • Codex, minorintegrationTests/security/choose-operation-authz.test.ts: no end-to-end case proves the export worker's nested-SQL recheck can deny an unauthorized query. The allowed super-user path reaching COMPLETE is covered, and unitTests/sqlTranslator/processAST.test.js proves the object-shaped denial stops routing, but the full dispatch → principal-propagation → reparse → denial path isn't exercised together. Worth adding a non-super-user unauthorized-export-SQL case that asserts denial.
  • Codex, nit — same file: trim added comments that narrate tests / restate identifiers / address the reviewer (the suite catalog + run-command block at the top, and identifier restatements).

@cb1kenobi cb1kenobi added the area:security Security, TLS/certs, authentication, authorization label Aug 19, 2026
@cb1kenobi
cb1kenobi requested review from heskew and kriszyp August 19, 2026 15:00

@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 strengthens the security and authorization model of the operations API, specifically within chooseOperation. It ensures that the authenticated principal is always enforced and prevents body-supplied parameters (like nested hdb_user or parsed_sql_object) from bypassing authorization checks. Additionally, it fixes a critical bug in processAST where permission denials (which are objects lacking a length property) were not correctly blocked. The review feedback suggests a minor optimization in chooseOperation to avoid polluting the top-level request object with parsed_sql_object when executing nested SQL searches, as it is only consumed by direct SQL operations.

Comment thread server/serverHelpers/serverUtilities.ts Outdated
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@cb1kenobi
cb1kenobi marked this pull request as ready for review August 19, 2026 16:17
@dawsontoth

Copy link
Copy Markdown
Contributor

We'll have some overlap with #2202 on your point 4 -- more details to come shortly!

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, glad to be reviewing the auth on SQL, looks like some good improvements. It looks like there are a few things to address (and maybe needs a rebase).

🤖 Reviewed with Codex

Comment thread server/serverHelpers/serverUtilities.ts Outdated
Comment thread server/serverHelpers/serverUtilities.ts Outdated
Comment thread server/serverHelpers/serverUtilities.ts
Comment thread server/serverHelpers/serverUtilities.ts
@cb1kenobi

Copy link
Copy Markdown
Member Author

Rebased onto main and worked through the review. Since this branch was opened, main independently landed several overlapping pieces (#2202 SQL denial fix, #2176/#2260 scoped tokens + operations allowlist), so the reconciliation keeps main's structure and layers this PR's hardening on top rather than re-introducing the older shape.

What changed in the reconciliation

  • Principal from authentication — a nested hdb_user is overwritten, never backfilled.
  • search_operation as subject only for exports — gated to export_local/export_to_s3, must be an object, and must name a supported export operation (search_by_value/search_by_hash/search_by_conditions/sql); anything else (including {}) is a request-time 400.
  • Outer + nested authorized separately — the export invocation is authorized first, then the nested search is authorized additively against its real handler and the authenticated principal, so a role granted export_local but not the underlying read is denied. NESTED-NOSQL is flipped to a denial; POSITIVE-NOSQL covers the permitted case.
  • SQL export routed through verifyPerms — so its requires_su gate applies exactly as on the non-SQL path (main's SQL branch checked only allowlist + AST, so a non-super user could export via a SQL search_operation). The northwind SQL-export cases now assert the requires_su denial.
  • Gemini's suggestion (only assign json.parsed_sql_object on the direct-SQL path) is also implemented.

Testingnpm run build clean; the authz/SQL unit suites pass (operation_authorization, tokenOperationScope, processAST, serverUtilities = 200+ specs green). I could not run the integration suites locally (this machine lacks the loopback alias pool), so choose-operation-authz.test.ts and the northwind changes will get their first real run in CI — I did validate the two load-bearing behaviors (SQL export → requires_su denial for a non-super user; export_local-granted role without read → nested denial) directly against the built verifyPerms.

One open question flagged inline on the nested-read thread: the nested check re-applies gate 1, making non-SQL export slightly stricter than SQL export for roles with a restrictive operations allowlist. Fail-closed, but happy to converge them if you'd prefer.

🤖 Generated by Barber AI (Opus 4.8)

@cb1kenobi
cb1kenobi force-pushed the fix/choose-operation-authz branch from 2df2eb2 to 1235c38 Compare September 3, 2026 22:14
@cb1kenobi
cb1kenobi requested a review from kriszyp September 3, 2026 22:21
Comment thread integrationTests/security/choose-operation-authz.test.ts
@cb1kenobi
cb1kenobi force-pushed the fix/choose-operation-authz branch from 8f56329 to 14ac88f Compare September 4, 2026 16:20
cb1kenobi and others added 2 commits September 8, 2026 17:47
…ted principal

chooseOperation handed verifyPerms the caller-supplied search_operation, making
both halves of the permission question — principal and tables — body-controlled.
Rebased onto main, which independently landed the SQL operations-allowlist check,
apiOperation token-scope threading (#2176, #2260), and the processAST denial fix
(#2202). This layers the principal/subject hardening on top and closes the gaps
the original PR had deferred:

- Principal comes from authentication: a nested hdb_user is overwritten, never
  backfilled.
- search_operation stands in as the permission subject only for the export
  operations that consume it, must be an object, and must name a supported export
  operation (search_by_value/hash/conditions/sql) — a primitive, {}, or an
  unsupported op is a request-time 400.
- One verifyPerms call cannot authorize both the outer export and its nested
  query: the outer op is authorized first, then the nested search is authorized
  additively against its real search handler and the authenticated principal, so
  a role granted export_local but not the underlying read is denied (previously
  granted at verifyPerms gate 2 before any table check).
- SQL export is routed through verifyPerms too, so its requires_su gate is
  enforced exactly as on the non-SQL path — SQL is no longer a way around it.
  (main's SQL branch checked only the allowlist + AST, so a non-super user could
  export via a SQL search_operation.)

Regression cover in integrationTests/security/choose-operation-authz.test.ts:
NESTED-NOSQL now asserts denial, NESTED-OP covers empty/invalid nested operations,
POSITIVE-NOSQL proves a permitted non-SQL export still completes; and the northwind
SQL-export cases now assert the requires_su denial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mallory's role sets an `operations` allowlist that excludes both add_user and
export_local, so gate 1 (verifyOperationsAllowlist on the top-level op) denies
her in both the fixed and a reverted dispatch — the two tests passed without
exercising the routing fix at all. Switch both to TABLER, which declares no
`operations`: gate 1 is a no-op, so the only barrier is that a nested
`operation: 'sql'` must not route the outer op past its verifyPerms/requires_su
check. Reverting the scoping fix now creates a super_user account (DISPATCH) or
lets a requires_su export through (DISPATCH-SU), so the tests fail closed.

Not EXPORTER for DISPATCH-SU: it lists export_local, which gate 2 grants, so it
is legitimately allowed to export (200) and would not discriminate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cb1kenobi
cb1kenobi force-pushed the fix/choose-operation-authz branch from 1d406e7 to f207347 Compare September 8, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:security Security, TLS/certs, authentication, authorization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants