Skip to content

Commit a6be285

Browse files
authored
S3: fix casing of PreSignedPost validation (#12449)
1 parent ac0ff24 commit a6be285

File tree

3 files changed

+70
-11
lines changed

3 files changed

+70
-11
lines changed

localstack-core/localstack/services/s3/presigned_url.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
SIGNATURE_V2_POST_FIELDS = [
6262
"signature",
63-
"AWSAccessKeyId",
63+
"awsaccesskeyid",
6464
]
6565

6666
SIGNATURE_V4_POST_FIELDS = [
@@ -768,13 +768,17 @@ def validate_post_policy(
768768
)
769769
raise ex
770770

771-
if not (policy := request_form.get("policy")):
771+
form_dict = {k.lower(): v for k, v in request_form.items()}
772+
773+
policy = form_dict.get("policy")
774+
if not policy:
772775
# A POST request needs a policy except if the bucket is publicly writable
773776
return
774777

775778
# TODO: this does validation of fields only for now
776-
is_v4 = _is_match_with_signature_fields(request_form, SIGNATURE_V4_POST_FIELDS)
777-
is_v2 = _is_match_with_signature_fields(request_form, SIGNATURE_V2_POST_FIELDS)
779+
is_v4 = _is_match_with_signature_fields(form_dict, SIGNATURE_V4_POST_FIELDS)
780+
is_v2 = _is_match_with_signature_fields(form_dict, SIGNATURE_V2_POST_FIELDS)
781+
778782
if not is_v2 and not is_v4:
779783
ex: AccessDenied = AccessDenied("Access Denied")
780784
ex.HostId = FAKE_HOST_ID
@@ -784,7 +788,7 @@ def validate_post_policy(
784788
policy_decoded = json.loads(base64.b64decode(policy).decode("utf-8"))
785789
except ValueError:
786790
# this means the policy has been tampered with
787-
signature = request_form.get("signature") if is_v2 else request_form.get("x-amz-signature")
791+
signature = form_dict.get("signature") if is_v2 else form_dict.get("x-amz-signature")
788792
credentials = get_credentials_from_parameters(request_form, "us-east-1")
789793
ex: SignatureDoesNotMatch = create_signature_does_not_match_sig_v2(
790794
request_signature=signature,
@@ -813,7 +817,6 @@ def validate_post_policy(
813817
return
814818

815819
conditions = policy_decoded.get("conditions", [])
816-
form_dict = {k.lower(): v for k, v in request_form.items()}
817820
for condition in conditions:
818821
if not _verify_condition(condition, form_dict, additional_policy_metadata):
819822
str_condition = str(condition).replace("'", '"')
@@ -896,7 +899,7 @@ def _parse_policy_expiration_date(expiration_string: str) -> datetime.datetime:
896899

897900

898901
def _is_match_with_signature_fields(
899-
request_form: ImmutableMultiDict, signature_fields: list[str]
902+
request_form: dict[str, str], signature_fields: list[str]
900903
) -> bool:
901904
"""
902905
Checks if the form contains at least one of the required fields passed in `signature_fields`
@@ -910,12 +913,13 @@ def _is_match_with_signature_fields(
910913
for p in signature_fields:
911914
if p not in request_form:
912915
LOG.info("POST pre-sign missing fields")
913-
# .capitalize() does not work here, because of AWSAccessKeyId casing
914916
argument_name = (
915-
capitalize_header_name_from_snake_case(p)
916-
if "-" in p
917-
else f"{p[0].upper()}{p[1:]}"
917+
capitalize_header_name_from_snake_case(p) if "-" in p else p.capitalize()
918918
)
919+
# AWSAccessKeyId is a special case
920+
if argument_name == "Awsaccesskeyid":
921+
argument_name = "AWSAccessKeyId"
922+
919923
ex: InvalidArgument = _create_invalid_argument_exc(
920924
message=f"Bucket POST must contain a field named '{argument_name}'. If it is specified, please check the order of the fields.",
921925
name=argument_name,

tests/aws/services/s3/test_s3.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11150,6 +11150,55 @@ def test_presigned_post_with_different_user_credentials(
1115011150
get_obj = aws_client.s3.get_object(Bucket=bucket_name, Key=object_key)
1115111151
snapshot.match("get-obj", get_obj)
1115211152

11153+
@markers.aws.validated
11154+
@pytest.mark.parametrize(
11155+
"signature_version",
11156+
["s3", "s3v4"],
11157+
)
11158+
def test_post_object_policy_casing(self, s3_bucket, signature_version):
11159+
object_key = "validate-policy-casing"
11160+
presigned_client = _s3_client_pre_signed_client(
11161+
Config(signature_version=signature_version),
11162+
endpoint_url=_endpoint_url(),
11163+
)
11164+
presigned_request = presigned_client.generate_presigned_post(
11165+
Bucket=s3_bucket,
11166+
Key=object_key,
11167+
ExpiresIn=60,
11168+
Conditions=[
11169+
{"bucket": s3_bucket},
11170+
["content-length-range", 5, 10],
11171+
],
11172+
)
11173+
11174+
# test that we can change the casing of the Policy field
11175+
fields = presigned_request["fields"]
11176+
fields["Policy"] = fields.pop("policy")
11177+
response = requests.post(
11178+
presigned_request["url"],
11179+
data=fields,
11180+
files={"file": "a" * 5},
11181+
verify=False,
11182+
)
11183+
assert response.status_code == 204
11184+
11185+
# test that we can change the casing of the credentials field
11186+
if signature_version == "s3":
11187+
field_name = "AWSAccessKeyId"
11188+
new_field_name = "awsaccesskeyid"
11189+
else:
11190+
field_name = "x-amz-credential"
11191+
new_field_name = "X-Amz-Credential"
11192+
11193+
fields[new_field_name] = fields.pop(field_name)
11194+
response = requests.post(
11195+
presigned_request["url"],
11196+
data=fields,
11197+
files={"file": "a" * 5},
11198+
verify=False,
11199+
)
11200+
assert response.status_code == 204
11201+
1115311202

1115411203
# LocalStack does not apply encryption, so the ETag is different
1115511204
@markers.snapshot.skip_snapshot_verify(paths=["$..ETag"])

tests/aws/services/s3/test_s3.validation.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,12 @@
710710
"tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_default_checksum": {
711711
"last_validated_date": "2025-03-17T21:46:24+00:00"
712712
},
713+
"tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_casing[s3]": {
714+
"last_validated_date": "2025-03-28T19:11:34+00:00"
715+
},
716+
"tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_casing[s3v4]": {
717+
"last_validated_date": "2025-03-28T19:11:36+00:00"
718+
},
713719
"tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_eq": {
714720
"last_validated_date": "2025-03-17T20:16:55+00:00"
715721
},

0 commit comments

Comments
 (0)