Repository navigation
Refuse unsigned x-amz-* headers and require the lock permissions at write time - #104
Merged
Merged
Conversation
…rite time Only the headers named in SignedHeaders reach the canonical request, but the handlers act on every header present. A request, above all a presigned URL in someone else's hands, could therefore carry x-amz-* terms its signer never approved. verify_request now refuses any x-amz-* header the signature does not cover, for header and presigned authentication alike, before the credential lookup. It answers AccessDenied with the message AWS uses. For header authentication this also makes x-amz-date and x-amz-content-sha256 signed. Content-MD5 may stay unsigned: it can only make the server refuse a body. Object Lock headers on a PUT, copy or multipart initiation now also require s3:PutObjectRetention (mode, retain-until date) or s3:PutObjectLegalHold (legal hold), as setting the same lock afterwards does. A bucket default retention still needs only s3:PutObject. A copy source naming ?versionId= requires s3:GetObjectVersion rather than s3:GetObject.
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
Closes two authorization gaps on the S3 data plane.
What was exploitable
x-amz-*headers were acted on. Only the headers named inSignedHeaders/X-Amz-SignedHeadersgo into the canonical request, but thehandlers read every header present. Anyone holding a presigned PUT URL could
therefore add headers the signer never approved and have them honoured under the
signer's identity: a copy source (turning the upload into a copy of anything the
signer can read), custom metadata, and on
mainObject Lock headers. AWS refusessuch requests.
s3:PutObject(mainonly, not in anyrelease). The lock headers on a PUT, copy, or multipart initiation added no
permission, so any writer on a lock-enabled bucket could create
COMPLIANCEversions nobody can delete.
s3:GetObject, nots3:GetObjectVersion, so an account limited to current objects could read anyversion by copying it.
What changed
sigv4::reject_unsigned_amz_headersrefuses any request carrying anx-amz-*headerthat is not in the signed-header list.
verify_requestcalls it for both header-authand presigned requests, right after parsing and before the credential lookup, so
these requests never reach the secret store. Because header auth already requires
x-amz-dateandx-amz-content-sha256to be present, this also requires them to besigned. Presigned
X-Amz-*query parameters are not headers and are unaffected.S3ErrorKind::UnsignedAmzHeaderanswers403 AccessDeniedwith AWS's message,There were headers present in the request which were not signed. This follows theexisting lock-refusal kinds: the code a client branches on stays standard, and the
message tells the operator why.
request_permissionsaddss3:PutObjectRetentionwhenx-amz-object-lock-modeorx-amz-object-lock-retain-until-dateis present on a keyed request, ands3:PutObjectLegalHoldwhenx-amz-object-lock-legal-holdis present. This followsthe existing bypass pattern. It covers PutObject, CopyObject, and
CreateMultipartUpload (where a multipart lock is fixed). The check is on presence, not
value:
legal-hold: OFFis an explicit lock choice that replaces the bucket default.A bucket default retention still needs only
s3:PutObject.?versionId=requiress3:GetObjectVersionon the source.Content-MD5on presigned requests: deliberately not required to be signed. It canonly cause the server to refuse a body the holder already chose, so leaving it unsigned
grants nothing. AWS does not require it to be signed either.
Allowlisted headers
None. Every
x-amz-*header must be signed. The boto3, JavaScript v3, Go v2 and Java v2compatibility suites pass unchanged: the SDKs sign every
x-amz-*header they send, andthe suites' presigned URLs are sent by plain HTTP clients that add none.
Compatibility impact (potentially breaking)
x-amz-*header is now refused. This affectshand-built requests and presigned-URL flows where the uploader adds headers; the fix
is to sign the URL with those headers.
s3:PutObject, or copied namedversions with only
s3:GetObject, needss3:PutObjectRetention/s3:PutObjectLegalHold/s3:GetObjectVersion.Tests
New tests in
crates/record-store-s3/src/auth.rs:x-amz-object-lock-mode: COMPLIANCEis refused andnothing is stored. The same request with an unknown access key is refused as unsigned,
not reported as
InvalidAccessKeyId, which shows it was checked before the lookup.x-amz-copy-sourceis refused, and nothing is copied.x-amz-meta-foois refused. The same request withthe header signed succeeds.
x-amz-content-sha256orx-amz-dateunsigned is refused.s3:PutObjectRetentionfor both PUT andmultipart initiation, and allowed with it.
ONandOFF): refused withouts3:PutObjectLegalHold,and allowed with it.
PutObject-only writer succeeds and the default ismaterialized.
?versionId=is refused withouts3:GetObjectVersion, a current-versioncopy still works, and the versioned copy works once the permission is granted.
test_support::signed_request_leaving_unsignedsigns a request while leaving the namedheaders out of
SignedHeaders. The signature over the rest is genuine, so a refusal canonly come from the new check.
signed_requestalready signed every header it set, andno existing test sent an unsigned
x-amz-*header, so no existing test changed.With the fix disabled, 7 of the 8 new tests fail. The default-retention test passes
either way, as intended: it guards against over-tightening.
cargo fmt --all --checkcargo clippy --workspace --all-targets --all-features --locked -- -D warningscargo test --workspace --all-features --lockedcargo +nightly fuzz build(SigV4 targets), plustests/fuzz-smoke.shtests/compatibility/run.sh(boto3, JS v3, Go v2, Java v2)Docs
docs/administration/policies.md,docs/administration/object-lock.md,docs/reference/s3-compatibility.md,docs/reference/errors.md, and aCHANGELOG.mdSecurity entry flagged as potentially breaking.