Skip to content

fix(s3): enforce presigned POST policy conditions with case-insensitive field lookup#2

Open
Meemaw wants to merge 3 commits intomainfrom
devin/1775299142-s3-presigned-post-policy-conditions
Open

fix(s3): enforce presigned POST policy conditions with case-insensitive field lookup#2
Meemaw wants to merge 3 commits intomainfrom
devin/1775299142-s3-presigned-post-policy-conditions

Conversation

@Meemaw
Copy link
Copy Markdown
Member

@Meemaw Meemaw commented Apr 4, 2026

Summary

Presigned POST uploads were only validating content-length-range policy conditions. All other conditions (bucket, key, Content-Type, etc.) were silently ignored, allowing uploads that violate the policy.

This PR replaces the narrow validateContentLengthRange method with validatePolicyConditions, which enforces:

  • Object-form exact match: {"bucket": "X"}, {"key": "X"}, {"Content-Type": "X"}
  • Array-form eq: ["eq", "$Content-Type", "image/png"]
  • Array-form starts-with: ["starts-with", "$key", "uploads/"]
  • Array-form 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 AccessDenied with AWS-compatible error messages:

Invalid according to Policy: Policy Condition failed: ["eq", "$Content-Type", "image/png"]

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

  • Bug fix (fix:)

AWS Compatibility

Incorrect behavior: Floci accepted presigned POST uploads regardless of policy conditions (except content-length-range). Real AWS S3 rejects with 403 AccessDenied when 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 failed starts-with prefix 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 lcFields map 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 403
    • presignedPostSucceedsWithCapitalPFieldName — sends "Policy" with valid data, asserts 204

Things to review carefully

  1. lcFields scope — the lowercased map is only used for policy validation. The Content-Type lookup for setting the stored object's content type (line ~1628) still uses the original fields map 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.
  2. Error messages preserve original casing — when a policy condition fails, the error message uses the field name from the policy JSON (e.g. "Content-Type"), not the lowercased version. This matches AWS behavior.
  3. asLong() for content-length-range boundsJsonNode.asLong() returns 0 for non-numeric nodes. If a malformed policy somehow has string values for min/max, the range check would silently use 0. Real SDK-generated policies always emit numeric values here.

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added
  • Commit messages follow Conventional Commits

Link to Devin session: https://app.devin.ai/sessions/06b7e28f3e664783b17ed18060e5c267
Requested by: @Meemaw


Open with Devin

@devin-ai-integration
Copy link
Copy Markdown

Original prompt from Matej

switch from localstsvk to floci. Minimal vode changes, just change the container image

@devin-ai-integration
Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@devin-ai-integration devin-ai-integration bot force-pushed the devin/1775299142-s3-presigned-post-policy-conditions branch from a70433a to 6351e75 Compare April 4, 2026 11:23
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

@Meemaw Meemaw marked this pull request as ready for review April 4, 2026 11:31
@Meemaw Meemaw force-pushed the devin/1775299142-s3-presigned-post-policy-conditions branch from 5d4268f to e6943aa Compare April 6, 2026 12:37
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>
@devin-ai-integration devin-ai-integration bot force-pushed the devin/1775299142-s3-presigned-post-policy-conditions branch from afb472e to 5b73ba9 Compare April 8, 2026 12:29
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration bot and others added 2 commits April 8, 2026 12:33
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>
@devin-ai-integration devin-ai-integration bot changed the title fix(s3): enforce presigned POST policy conditions (eq, starts-with, content-type) fix(s3): enforce presigned POST policy conditions with case-insensitive field lookup Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant