fix(vault): cap secret body reads - #19
Conversation
Vault put() already rejects secrets larger than 16 KiB. get() called unbounded object.text(), so a large R2 or S3 object could OOM the worker. Cap envelope reads at 32 KiB (enough for a 16 KiB secret) and apply the same cap to AWS consumeBody. Streamed .body stays uncapped for appliance archives. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs real behavior proof before merge. Reviewed August 15, 2026, 10:29 PM ET / August 16, 2026, 02:29 UTC. ClawSweeper reviewWhat this changesThe PR caps OAuth credential envelope reads at 32 KiB and adds regressions for oversized and maximum-size credential envelopes. Merge readiness⛔ Blocked until real behavior proof from a real setup is added - 4 items remain Keep open: the vault-boundary fix is focused and appears correct, but it needs real R2 or S3 after-fix proof before merge. Likely related people: Peter Steinberger (high confidence). Priority: P2 Review scores
Verification
How this fits togetherOAuth credentials are encrypted into small JSON envelopes in a private R2 or S3 vault. OAuth callback, revocation, and invocation paths read and decrypt those envelopes before using the credential with GitHub. flowchart LR
A[OAuth request] --> B[OAuth vault]
B --> C[Private R2 or S3 object]
C --> D{Envelope within 32 KiB?}
D -->|yes| E[Decrypt credential]
D -->|no| F[Reject oversized envelope]
E --> G[GitHub credential use]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Merge the vault-only cap after redacted real-provider output shows oversized rejection and successful decryption of a valid maximum-size credential envelope. Do we have a high-confidence way to reproduce the issue? Yes for the source defect: current main directly calls the vault object's unbounded text reader, and the PR regressions construct an oversized object for that path. A real cloud-provider reproduction is not attached. Is this the best way to solve the issue? Yes: limiting reads at OAuthVault protects credential retrieval while preserving general object and appliance streaming behavior. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against 71b80d86ca0b. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (1 earlier review cycle)
|
What Problem This Solves
Vault
put()rejects OAuth secrets larger than 16 KiB. Vaultget()then calls unboundedobject.text(). A large object in the private R2 or S3 vault can inflate the Worker or ECS task until it OOMs. AWSconsumeBodyhas the same missing cap, sotext()/arrayBuffer()/json()on the S3 adapter will buffer any size.Evidence
Live Node against the production
OAuthVault.get()andAwsS3Bucket.text()helpers.Patched call sites on this branch:
A 1 MiB vault object is rejected before
text()runs. A 64 KiB S3text()read is rejected. Streamed.bodystill returns the full 65536 bytes so appliance archives stay on the uncapped path. A 16 KiB secret still round-trips (envelope 21953 bytes, under 32 KiB).Real behavior proof
get()and AWSconsumeBodynow refuse secret-body reads above 32 KiB. A large R2 or S3 object fails withOAuth credential envelope is too large/object body exceeds the read limitinstead of buffering the whole object./tmp/oc-impl-crabhelm-vaultonfix/vault-body-cap. Livenodeagainst the productionOAuthVaultandAwsS3Bucketclasses.size=1MiBwhosetext()would allocate 1 MiB. Called productionOAuthVault.get(). Put a 64 KiB object throughAwsS3Bucketand calledtext()vs streamed.body. Round-tripped a 16 KiB secret throughput()/get().get()threw beforetext()(text_calls=0). S3text()hit the 32 KiB consume cap. Streamed.bodystill delivered 65536 bytes. A max-size 16 KiB secret still decrypted (envelope_bytes=21953)..text()(archives use streamed.bodyinhandleBootstrap).Summary
OAuthVault.get()rejectsobject.sizeabove 32 KiB and stream-reads with the same capconsumeBodyuses the same 32 KiB cap; streamed.bodystays uncapped for appliance tarballsobject.text()landed in775ef3d(2026-07-02) and has been present for 44 daysconsumeBodylanded in openclaw/crabhelm#8 (94d9d64, 2026-07-07)aws/alb-identity.tsandboundedProviderJson