Skip to content

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 into
fix/batches-delete-completed-authzfrom
fix/batches-delete-completed-revoked-key
Sep 11, 2026
Merged

diegosouzapw merged 1 commit into
fix/batches-delete-completed-authzfrom
fix/batches-delete-completed-revoked-key

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Second pass of the review batteries on #12969, this time /omni-code-sec (4 security lenses: built-in security-review, claude-security, ToB differential-review, ToB insecure-defaults). Closes the two findings the owner approved for an immediate fix; the third one is an auth-architecture decision tracked separately.

  • SEC-B (high, CWE-613, 3 reviewers) — the fail-closed gate added in fix(api): explicit scope, audit log and atomic sweep for DELETE /v1/batches/delete-completed (omni-code-review battery on #12969) #13262 authorized a presented key by row existence (getApiKeyMetadata), not validity: a revoked, deactivated, banned or expired key still resolved an apiKeyId and ran the sweep. The route now also requires validateApiKey() (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.
  • SEC-E (nit, Hard Rule fix(ui): fix Select dropdown dark theme inconsistency #12) — both 401 bodies now go through buildErrorBody(401, …) (type: authentication_error, code: invalid_api_key) instead of a hand-built object.
  • SEC-F (low) — 4 negative route tests: revoked, deactivated, banned, expired (the last one alongside a session cookie, proving the session branch is never reached); the unauthenticated case now asserts the buildErrorBody shape.

Not in this PR (owner decisions):

Validation

node --import tsx/esm --test tests/unit/batch-deletion.test.ts \
  tests/unit/batches-delete-completed-ownership-wvxc.test.ts \
  tests/unit/batches-delete-completed-route-scope.test.ts tests/unit/api-keys*.test.ts
# pass 32 / fail 0  (route-scope: 5 red before the fix → 10 green after)
npm run typecheck:core                      # OK
npx eslint <changed files>                  # 0 errors
CHANGELOG_BASE_REF=origin/fix/batches-delete-completed-authz npm run check:changelog-integrity  # OK

Full suite on the .113 box: see the last comment.

⚠️ base-red inherited: #12732 (ESLint ×3, README migration count) — none touched here.

Refs #12969 · review record: _tasks/reviews/omni-code-sec/2026-09-10_fix-batches-delete-completed-authz_vs_release-v3.8.51/

…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
@diegosouzapw

Copy link
Copy Markdown
Owner Author

Full suite on the .113 box (b6d2f0f, own worktree, npm ci)

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
diegosouzapw merged commit 1eac022 into fix/batches-delete-completed-authz Sep 11, 2026
3 checks passed
@diegosouzapw
diegosouzapw deleted the fix/batches-delete-completed-revoked-key branch September 11, 2026 16:07
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
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
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.

1 participant