fix(api): reject revoked, deactivated, banned or expired keys on DELETE /v1/batches/delete-completed (omni-code-sec on #12969) - #13297
Merged
diegosouzapw merged 1 commit intoSep 11, 2026
Conversation
…TE /v1/batches/delete-completed The fail-closed gate added in #13262 authorized a presented key by row EXISTENCE (getApiKeyMetadata); a revoked/deactivated/banned/expired key still has a row and ran the sweep (CWE-613). The route now also requires validateApiKey() — the one lifecycle gate — before choosing a scope, and neither an unresolved nor an invalid key falls through to the session branch. Both 401 bodies now go through buildErrorBody() (Hard Rule #12). Found by the omni-code-sec battery on #12969 (SEC-B, SEC-E, SEC-F); 4 negative route tests added (revoked, deactivated, banned, expired — the last one alongside a session cookie). Refs #12969
Owner
Author
Full suite on the .113 box (
|
| suite | result |
|---|---|
test:unit |
37 731 pass / 43 fail / 27 skipped — 0 failures in batches or api-keys; the 4 new negative tests pass in the full run |
test:vitest |
51 files / 465 tests passed |
build |
standalone OK |
The 43 unit failures: 36 are the same inherited files recorded on #13262; image-generation-handler ×3 fails identically on the base 150ca009 in isolation; the remaining 7 (models-catalog-combo-metadata, specialty-model-catalog-routes, sanitizers.property, vscode-token-routes, conversationTracker-reconnect-7847, quota-weighted-strategy) only failed while vitest and build ran concurrently (load average 77) and pass on both the fix and the base with the box idle. No failure is introduced by this diff.
Focused: batches-delete-completed-route-scope 10/10 · batches-delete-completed-ownership-wvxc 8/8 · batch-deletion 8/8.
diegosouzapw
merged commit Sep 11, 2026
1eac022
into
fix/batches-delete-completed-authz
3 checks passed
This was referenced Sep 12, 2026
diegosouzapw
added a commit
that referenced
this pull request
Sep 14, 2026
Merged after reconciling the whole stack onto the tip, operator-reviewed before merge. The core ownership fix for GHSA-wvxc-jp3v-5mg5 had already landed via #13211 with a different implementation of the same endpoint. This branch carries a stricter one, built up in layers: this PR, #13262 (explicit scope, audit log, atomic sweep), #13297 (revoked, deactivated, banned or expired keys rejected) and #13374 (owner-scoped file half, chunked instance sweep — SEC-C/SEC-D). The stack's implementation was kept over #13211's because it is stricter on every point: - **route:** with #13211, a request carrying BOTH a dashboard session cookie and an API key swept the whole instance. Here a presented key always scopes the sweep to that key; only a session without a key sweeps all tenants; neither returns 401. Instance-wide and row-deleting sweeps log at warn as an audit trail, and a failing sweep returns a sanitized 500. - **`deleteCompletedBatches`:** takes an explicit `{ apiKeyId } | { allTenants: true }`. An omitted or blank key throws instead of widening, and passing both throws. - **files:** a key sweep only soft-deletes files the caller owns (`deleteFileOwnedBy`), so a batch referencing another tenant's or an unowned file never nulls its content. - **transactions:** key mode is all-or-nothing across chunks; instance mode commits per 200-id chunk so a large sweep never holds one write lock on the table. #13211's own test was aligned to the explicit-scope API with its assertions unchanged. Its seed now creates the file with the batch's owner, as an upload through that key does in production — without that, SEC-C correctly leaves the unowned file intact. - 51/51 across the six batch suites (#13211's test, this stack's ownership and route-scope tests, `batch-deletion`, `batch-deletion-route-logic`, `files-delete-owned-by`) - ESLint, `typecheck:core`, complexity, cognitive-complexity, changelog integrity: clean⚠️ base-red inherited: #12732
This was referenced Sep 14, 2026
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…w#12969) Merged after reconciling the whole stack onto the tip, operator-reviewed before merge. The core ownership fix for GHSA-wvxc-jp3v-5mg5 had already landed via diegosouzapw#13211 with a different implementation of the same endpoint. This branch carries a stricter one, built up in layers: this PR, diegosouzapw#13262 (explicit scope, audit log, atomic sweep), diegosouzapw#13297 (revoked, deactivated, banned or expired keys rejected) and diegosouzapw#13374 (owner-scoped file half, chunked instance sweep — SEC-C/SEC-D). The stack's implementation was kept over diegosouzapw#13211's because it is stricter on every point: - **route:** with diegosouzapw#13211, a request carrying BOTH a dashboard session cookie and an API key swept the whole instance. Here a presented key always scopes the sweep to that key; only a session without a key sweeps all tenants; neither returns 401. Instance-wide and row-deleting sweeps log at warn as an audit trail, and a failing sweep returns a sanitized 500. - **`deleteCompletedBatches`:** takes an explicit `{ apiKeyId } | { allTenants: true }`. An omitted or blank key throws instead of widening, and passing both throws. - **files:** a key sweep only soft-deletes files the caller owns (`deleteFileOwnedBy`), so a batch referencing another tenant's or an unowned file never nulls its content. - **transactions:** key mode is all-or-nothing across chunks; instance mode commits per 200-id chunk so a large sweep never holds one write lock on the table. diegosouzapw#13211's own test was aligned to the explicit-scope API with its assertions unchanged. Its seed now creates the file with the batch's owner, as an upload through that key does in production — without that, SEC-C correctly leaves the unowned file intact. - 51/51 across the six batch suites (diegosouzapw#13211's test, this stack's ownership and route-scope tests, `batch-deletion`, `batch-deletion-route-logic`, `files-delete-owned-by`) - ESLint, `typecheck:core`, complexity, cognitive-complexity, changelog integrity: clean⚠️ base-red inherited: diegosouzapw#12732
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second pass of the review batteries on #12969, this time
/omni-code-sec(4 security lenses: built-insecurity-review,claude-security, ToBdifferential-review, ToBinsecure-defaults). Closes the two findings the owner approved for an immediate fix; the third one is an auth-architecture decision tracked separately.getApiKeyMetadata), not validity: a revoked, deactivated, banned or expired key still resolved anapiKeyIdand ran the sweep. The route now also requiresvalidateApiKey()(the one lifecycle gate:is_active,revoked_at,is_banned,expires_at) before choosing a scope. Neither an unresolved nor an invalid key falls through to the session branch.buildErrorBody(401, …)(type: authentication_error,code: invalid_api_key) instead of a hand-built object.buildErrorBodyshape.Not in this PR (owner decisions):
JWT_SECRET, and the public Cursor CLI key-exchange endpoint mints JWTs with the same secret → forgeableauth_token→ instance-wide sweep. Auth architecture, tracked in security(auth): any JWT signed with JWT_SECRET is a dashboard session — Cursor CLI key-exchange tokens can forge auth_token #13298.Validation
Full suite on the .113 box: see the last comment.
Refs #12969 · review record:
_tasks/reviews/omni-code-sec/2026-09-10_fix-batches-delete-completed-authz_vs_release-v3.8.51/