feat(oci): body-size cap on all push paths (DoS hardening, stacked on #84)#85
Closed
jamestexas wants to merge 1 commit into
Closed
feat(oci): body-size cap on all push paths (DoS hardening, stacked on #84)#85jamestexas wants to merge 1 commit into
jamestexas wants to merge 1 commit into
Conversation
…dening for cloister-667ea6)
Closes the highest-priority finding from cloister-667ea6 adversarial
review (dos-resilience-auditor): push paths called arrayBuffer()
unconditionally, with no Content-Length precheck and no size cap. A
single push of an N-byte body materialized ~3N bytes in isolate
memory before any hashing ran, exposing the shared isolate to
cross-tenant memory-exhaustion attacks.
Approach (two-layer defense):
1. **Cheap header check** (checkContentLengthHeader): rejects on the
client's Content-Length header alone, BEFORE arrayBuffer() runs.
Standard clients send this; if it claims > cap, return 413
PAYLOAD_TOO_LARGE (OCI SIZE_INVALID) without paying any
buffering cost.
2. **Post-buffer actual-size check** (checkActualSize): catches
clients that omit Content-Length or spoof it under the cap then
send more. Returns the same 413.
Both checks applied at all five push call sites in OciRegistryRoute:
- Monolithic POST .../blobs/uploads/?digest=... (line 472)
- Chunked-begin POST .../blobs/uploads/ optional seed body (line 494)
- PATCH .../blobs/uploads/<uuid> — CUMULATIVE size check on
(session.size + chunk.byteLength), so paced PATCHes can't grow
a session past the cap incrementally (line 540)
- Chunked-finalize PUT .../blobs/uploads/<uuid>?digest=... — also
cumulative on trailing body (line 591)
- Manifest PUT .../manifests/<reference> (line 645)
Per-instance configurable via `new OciRegistryRoute({ maxBlobBytes })`,
default 256 MiB (DEFAULT_MAX_BLOB_BYTES). The default fits realistic
mache .db chunks and OCI image layers while keeping one push under
the workerd per-isolate heap budget. Env-binding override surface
deferred to a follow-up bead.
Tests: 5 new cases in oci-registry-push.test.ts §"body-size cap":
- monolithic body > cap → 413
- Content-Length header > cap (small actual body) → 413
- chunked PATCH cumulative > cap → 413
- manifest PUT body > cap → 413
- body exactly == cap → 201 (boundary inclusive)
Verification: task lint → 1141/1141 tests pass (was 1136, +5 body-cap).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 24, 2026
Contributor
Author
|
Superseded by #90 (consolidated build-cache/v1 storage-key + hardening). Closing to reduce review load. |
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.
Stacked on #84. Merge #83 → #84 → this.
Closes the highest-priority finding from cloister-667ea6's adversarial review (dos-resilience-auditor):
Approach
Two-layer defense:
Content-Lengthheader beforearrayBuffer()runs. Standard clients send this; over-cap requests reject in ms with zero buffering cost.Content-Lengthor spoof it. Same 413 PAYLOAD_TOO_LARGE response.Both checks applied at all 5 push call sites:
POST .../blobs/uploads/?digest=...POST .../blobs/uploads/(optional seed body)PATCH .../blobs/uploads/<uuid>— cumulative check on (session bytes so far + this chunk) so paced PATCHes can't grow a session past the capPUT .../blobs/uploads/<uuid>?digest=...— also cumulativePUT .../manifests/<reference>Configuration
Per-instance via
new OciRegistryRoute({ maxBlobBytes }). Default 256 MiB — fits realistic mache.dbchunks and OCI image layers while keeping one push under workerd's per-isolate heap budget. Env-binding override surface deferred (separate follow-up).Test plan
test/routes/oci-registry-push.test.ts§"body-size cap":Content-Lengthheader > cap (small actual body) → 413 (header-only fast path)task lint→ 1141/1141 tests pass (was 1136, +5 body-cap)What this does NOT close from cloister-667ea6
BlobStore.putsubstrate-level verification when key provided. Smaller; could land soon.TrustStore.upsertRegistryTagvalidation — recorded as comments on cloister-667ea6.🤖 Generated with Claude Code