fix(s3): enforce presigned POST policy conditions with case-insensitive field lookup#2
Open
fix(s3): enforce presigned POST policy conditions with case-insensitive field lookup#2
Conversation
Original prompt from Matej
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
a70433a to
6351e75
Compare
5d4268f to
e6943aa
Compare
The AWS SDK sends the presigned POST policy field as 'Policy' (capital P), but the validation code was looking for 'policy' (lowercase). This caused policy condition validation to be silently skipped for all presigned POST uploads made via the AWS SDK. Now checks both 'Policy' and 'policy' for compatibility. Co-Authored-By: Matej Snuderl <ematej.snuderl@gmail.com>
afb472e to
5b73ba9
Compare
Co-Authored-By: Matej Snuderl <ematej.snuderl@gmail.com>
…validation Normalize form field keys to lowercase before policy lookup and condition matching, matching the behaviour of LocalStack and real AWS S3. Previously only 'Policy' and 'policy' were checked; now any casing is handled correctly. Co-Authored-By: Matej Snuderl <ematej.snuderl@gmail.com>
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
Presigned POST uploads were only validating
content-length-rangepolicy conditions. All other conditions (bucket,key,Content-Type, etc.) were silently ignored, allowing uploads that violate the policy.This PR replaces the narrow
validateContentLengthRangemethod withvalidatePolicyConditions, which enforces:{"bucket": "X"},{"key": "X"},{"Content-Type": "X"}eq:["eq", "$Content-Type", "image/png"]starts-with:["starts-with", "$key", "uploads/"]content-length-range:["content-length-range", min, max](existing, preserved)Additionally, form field names are now normalized to lowercase before policy validation, matching the behavior of LocalStack and real AWS S3. The AWS SDK sends
"Policy"(capital P) while some clients use"policy"— both now work correctly.Policy violations return
403 AccessDeniedwith AWS-compatible error messages:Discovered via seadn#1921 — a unit test expects S3 to reject presigned POST uploads when Content-Type doesn't match the policy.
Type of change
fix:)AWS Compatibility
Incorrect behavior: Floci accepted presigned POST uploads regardless of policy conditions (except content-length-range). Real AWS S3 rejects with
403 AccessDeniedwhen any condition is not met. Additionally, the policy field was only looked up as"policy"(lowercase), but the AWS SDK sends it as"Policy"(capital P), silently skipping all validation.Corrected behavior: Now matches AWS S3 — mismatched
Content-Type,key,bucket, or failedstarts-withprefix checks return 403. Form field lookup is case-insensitive, matching LocalStack's approach (form_dict = {k.lower(): v for k, v in request_form.items()}).Updates since last revision
Policy JSON is now parsed via Jackson
ObjectMapper.readTree()→JsonNode, matching the pattern used elsewhere in the codebase (e.g.AwsJsonController). Removed all hand-rolled string manipulation helpers (findMatchingBracket,skipWhitespace,unquote,splitJsonArray).All error-path tests assert exact XML error responses via XPath instead of
containsString:contentType("application/xml")hasXPath("/Error/Code", equalTo(...))hasXPath("/Error/Message", equalTo(...))This applies to the 4 new policy-enforcement tests and the 4 pre-existing error tests (
EntityTooLarge,InvalidArgument×2,NoSuchBucket).Case-insensitive field lookup: all form field keys are lowercased into a separate
lcFieldsmap before policy validation. Both the policy field name ("Policy"/"policy") and condition field references ("Content-Type"→"content-type") are normalized. This matches LocalStack's implementation.Two new tests for capital-P
"Policy"field name:presignedPostEnforcesPolicyWithCapitalPFieldName— sends"Policy"with mismatched Content-Type, asserts 403presignedPostSucceedsWithCapitalPFieldName— sends"Policy"with valid data, asserts 204Things to review carefully
lcFieldsscope — the lowercased map is only used for policy validation. TheContent-Typelookup for setting the stored object's content type (line ~1628) still uses the originalfieldsmap with original casing. This is intentional (that lookup has always been case-sensitive) but worth verifying that SDK-generated forms always send"Content-Type"with that exact casing."Content-Type"), not the lowercased version. This matches AWS behavior.asLong()for content-length-range bounds —JsonNode.asLong()returns0for non-numeric nodes. If a malformed policy somehow has string values for min/max, the range check would silently use0. Real SDK-generated policies always emit numeric values here.Checklist
./mvnw testpasses locallyLink to Devin session: https://app.devin.ai/sessions/06b7e28f3e664783b17ed18060e5c267
Requested by: @Meemaw