Repository navigation
fix: reject keys for s3 storage if not configured - #3411
opieter-aws merged 3 commits into
Conversation
|
@strandly-the-agent review |
|
Assessment: Approve Well-scoped, correct security fix that closes the prefix-scope bypass in the context-offloader Review Categories
Nicely mirrored across both SDKs and the regression tests clearly reference the originating report. |
There was a problem hiding this comment.
REQUEST_CHANGES — the TypeScript change breaks persisted references created with leading-slash prefixes; preserve the existing key namespace while adding confinement.
✅ Reviewed exact head af3f2a86; CI rollup is green.
✅ Prefix-confinement attack matrices passed twice; focused Python/TS suites and TS build passed.
🔴 One compatibility blocker at strands-ts/src/vended-plugins/context-offloader/storage.ts:386, reproduced cold twice against head and merge-base.
🧭 This changes a public method’s accepted inputs/errors and requires the API-review label workflow; no API meeting is needed.
🧪 The nonblocking Python mock assertion is already covered by discussion_r3631980159; I haven’t duplicated it.
Blocker and verification ledger
🔴 Preserve the existing leading-slash namespace
At strands-ts/src/vended-plugins/context-offloader/storage.ts:386, prefix: "/artifacts" is now normalized to artifacts/. The previous implementation stored /artifacts/<id> and returned s3://b//artifacts/<id>; the parser at storage.ts:428-434 resolves that legacy URI to /artifacts/<id>, then the new check at storage.ts:436-438 rejects both the URI and raw legacy key before contacting S3. Please retain the historical trailing-only normalization while adding confinement, and handle any namespace migration separately rather than implicitly authorizing two namespaces.
- Head rejects legacy raw and URI references using
/artifacts/<id>and writes new keys asartifacts/<id>. - Merge-base reads both legacy forms and retains the leading slash on writes.
- Verified: Python S3 tests 13 passed; TS storage 32 passed; TS plugin 50 passed; TS build and
git diff --checkpassed. - Adversarial matrices covered 20 Python + 20 TypeScript cases, each twice. S3 clients preserve dot/slash key semantics; no confinement bypass reproduced.
- Advanced API/adversarial tiers were unavailable due retention-mode validation; both passes were retried on the default tier.
- Suppressed as pre-existing: TS broad error catch/tool-result status. TSDoc/security guidance would be useful follow-up work, but is not another blocker here.
strandly-the-agent
left a comment
There was a problem hiding this comment.
Inline compatibility finding attached to the existing changes-requested review.
|
@strandly-the-agent re-review |
strandly-the-agent
left a comment
There was a problem hiding this comment.
APPROVE — the TypeScript namespace compatibility blocker is resolved at 40c30078; no code blockers remain.
✅ Historical /artifacts/... writes are preserved and legacy s3://b//artifacts/foo resolves to /artifacts/foo.
✅ Prefix confinement remains unchanged; the prior blocker thread is resolved/outdated.
✅ Local: TS storage 32/32, build, full lint, full format check, and diff-check passed; clean tree.
✅ Exact-head CI: all Python/TS unit matrices, lint/quality/audit/package, Docs, and CI Gate passed.
pytest is unavailable; CI covered all 11 Python jobs.
🧭 Before merge, complete the required api/needs-review → api/review-complete label workflow; both labels are still absent.
Re-review scope
Reviewed only af3f2a86..40c30078. The delta restores the released trailing-slash-only normalization and adds a regression that independently reads the old double-slash URI and asserts the exact S3 key. It does not broaden reads or introduce a dual namespace.
The existing Python get_object.assert_not_called() suggestion remains in discussion_r3631980159; I did not duplicate or escalate that previously nonblocking test-hardening request.
Two integration jobs were still running during the detailed check; the final exact-head rollup is now SUCCESS.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Description
The context-offloader
S3Storagewrites every object under the configuredprefixinstore(), butretrieve()never re-checked that invariant: it accepted a raw key verbatim, and for ans3://URI it validated only the bucket before strippings3://bucket/and using the remainder as the key. Theretrieve_offloaded_contenttool (registered by default) passes a model-suppliedreferencestraight through, so an agent that takes untrusted input could be steered to read any object elsewhere in the same bucket that the SDK's credentials canGetObject— a prefix-scope bypass. This is the confused-deputy gap theprefixparameter implies protection against, and the siblingFileStorage.retrieve()already enforces the equivalent directory boundary.The fix adds the missing invariant in both SDKs: after resolving a reference to a key, reject it if a prefix is configured and the key falls outside it. Empty prefix keeps today's unrestricted behavior, and in-prefix keys and
store()-returned URIs are unaffected, so this is backward-compatible for every referencestore()produces. The newer unifiedstrands.storage.S3Storageis not affected — it prepends the prefix rather than parsing it from caller input — so no change there.Making
retrieve()prefix-sensitive surfaced a pre-existing normalization mismatch: the TS constructor stripped only trailing slashes fromprefixwhile Python'sstrip("/")strips both ends, so a leading-slash prefix (e.g."/artifacts") produced different scopes and non-round-trippingstore()keys across SDKs. The TS side now strips both ends to match Python, so the two SDKs normalize any prefix identically.Documentation PR
No documentation changes. The user guide already documents
S3Storagewithbucket/prefixand the default retrieval tool; behavior now matches that scope. A follow-up could add a defense-in-depth note recommending prefix-scoped IAM or a dedicated bucket.Type of Change
Bug fix
Testing
Python: ran the context-offloader unit suite (
hatch test tests/strands/vended_plugins/context_offloader/), which covers the S3 store/retrieve paths including the new raw-key ands3://-URI out-of-prefix rejections, and linted the changed files withhatch fmt --linter(ruff + mypy strict).TypeScript: ran the offloader storage and plugin unit suites (
vitest, node project), which cover the mirrored rejections and the retrieval-tool path, pluseslintandtsc --project src/tsconfig.jsonon the changed files.Reproduced the original bypass against the real
storage.pywith an in-memory fake S3 client, then confirmed post-fix that both attack forms raiseKeyError, the normalstore()→retrieve()round trip still returns content, and cross-bucket references remain rejected.I ran
hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.