8000 S3: fix IfMatch/IfNoneMatch in pre-signed URLs (#12624) · localstack/localstack@f3a4c74 · GitHub
[go: up one dir, main page]

Skip to content

Commit f3a4c74

Browse files
authored
S3: fix IfMatch/IfNoneMatch in pre-signed URLs (#12624)
1 parent 247c12d commit f3a4c74

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@
7070
"x-amz-date",
7171
]
7272

73+
# Boto3 has some issues with some headers that it disregards and does not validate or adds to the signature
74+
# we need to manually define them
75+
# see https://github.com/boto/boto3/issues/4367
76+
SIGNATURE_V4_BOTO_IGNORED_PARAMS = [
77+
"if-none-match",
78+
"if-match",
79+
]
80+
7381
# headers to blacklist from request_dict.signed_headers
7482
BLACKLISTED_HEADERS = ["X-Amz-Security-Token"]
7583

@@ -645,7 +653,10 @@ def _get_signed_headers_and_filtered_query_string(
645653
qs_param_low = qs_parameter.lower()
646654
if (
647655
qs_parameter not in SIGNATURE_V4_PARAMS
648-
and qs_param_low.startswith("x-amz-")
656+
and (
657+
qs_param_low.startswith("x-amz-")
658+
or qs_param_low in SIGNATURE_V4_BOTO_IGNORED_PARAMS
659+
)
649660
and qs_param_low not in headers
650661
):
651662
if qs_param_low in signed_headers:

tests/aws/services/s3/test_s3.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from zoneinfo import ZoneInfo
1919

2020
import boto3 as boto3
21+
import botocore
2122
import pytest
2223
import requests
2324
import xmltodict
@@ -7434,6 +7435,87 @@ def add_content_sha_header(request, **kwargs):
74347435
resp = requests.put(rewritten_url, data="something", verify=False)
74357436
assert resp.status_code == 403
74367437

7438+
@markers.aws.validated
7439+
def test_pre_signed_url_if_none_match(self, s3_bucket, aws_client, aws_session):
7440+
# there currently is a bug in Boto3: https://github.com/boto/boto3/issues/4367
7441+
# so we need to use botocore directly to allow testing of this, as other SDK like the Java SDK have the correct
7442+
# behavior
7443+
object_key = "temp.txt"
7444+
7445+
s3_endpoint_path_style = _endpoint_url()
7446+
7447+
# assert that the regular Boto3 client does not work, and does not sign the parameter as requested
7448+
client = _s3_client_pre_signed_client(
7449+
Config(signature_version="s3v4", s3={}),
7450+
endpoint_url=s3_endpoint_path_style,
7451+
)
7452+
bad_url = client.generate_presigned_url(
7453+
"put_object",
7454+
Params={"Bucket": s3_bucket, "Key": object_key, "IfNoneMatch": "*"},
7455+
)
7456+
assert "if-none-match=%2a" not in bad_url.lower()
7457+
7458+
req = botocore.awsrequest.AWSRequest(
7459+
method="PUT",
7460+
url=f"{s3_endpoint_path_style}/{s3_bucket}/{object_key}",
7461+
data={},
7462+
params={
7463+
"If-None-Match": "*",
7464+
},
7465+
headers={},
7466+
)
7467+
7468+
botocore.auth.S3SigV4QueryAuth(aws_session.get_credentials(), "s3", "us-east-1").add_auth(
7469+
req
7470+
)
7471+
7472+
assert "if-none-match=%2a" in req.url.lower()
7473+
7474+
response = requests.put(req.url)
7475+
assert response.status_code == 200
7476+
7477+
response = requests.put(req.url)
7478+
# we are now failing because the object already exists
7479+
assert response.status_code == 412
7480+
7481+
@markers.aws.validated
7482+
def test_pre_signed_url_if_match(self, s3_bucket, aws_client, aws_session):
7483+
key = "test-precondition"
7484+
aws_client.s3.put_object(Bucket=s3_bucket, Key=key, Body="test")
7485+
7486+
s3_endpoint_path_style = _endpoint_url()
7487+
# empty object ETag is provided
7488+
empty_object_etag = "d41d8cd98f00b204e9800998ecf8427e"
7489+
7490+
# assert that the regular Boto3 client does not work, and does not sign the parameter as requested
7491+
client = _s3_client_pre_signed_client(
7492+
Config(signature_version="s3v4", s3={}),
7493+
endpoint_url=s3_endpoint_path_style,
7494+
)
7495+
bad_url = client.generate_presigned_url(
7496+
"put_object",
7497+
Params={"Bucket": s3_bucket, "Key": key, "IfMatch": empty_object_etag},
7498+
)
7499+
assert "if-match=d41d8cd98f00b204e9800998ecf8427e" not in bad_url.lower()
7500+
7501+
req = botocore.awsrequest.AWSRequest(
7502+
method="PUT",
7503+
url=f"{s3_endpoint_path_style}/{s3_bucket}/{key}",
7504+
data={},
7505+
params={
7506+
"If-Match": empty_object_etag,
7507+
},
7508+
headers={},
7509+
)
7510+
7511+
botocore.auth.S3SigV4QueryAuth(aws_session.get_credentials(), "s3", "us-east-1").add_auth(
7512+
req
7513+
)
7514+
assert "if-match=d41d8cd98f00b204e9800998ecf8427e" in req.url.lower()
7515+
7516+
response = requests.put(req.url)
7517+
assert response.status_code == 412
7518+
74377519

74387520
class TestS3DeepArchive:
74397521
"""

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,12 @@
788788
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_forward_slash_bucket": {
789789
"last_validated_date": "2025-01-21T18:25:38+00:00"
790790
},
791+
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_if_match": {
792+
"last_validated_date": "2025-05-15T13:08:44+00:00"
793+
},
794+
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_if_none_match": {
795+
"last_validated_date": "2025-05-15T12:51:09+00:00"
796+
},
791797
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presign_with_additional_query_params": {
792798
"last_validated_date": "2025-01-21T18:22:43+00:00"
793799
},

0 commit comments

Comments
 (0)
0