Thanks to visit codestin.com
Credit goes to github.com

Skip to content

S3: fix IfMatch/IfNoneMatch in pre-signed URLs #12624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion localstack-core/localstack/services/s3/presigned_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@
"x-amz-date",
]

# Boto3 has some issues with some headers that it disregards and does not validate or adds to the signature
# we need to manually define them
# see https://github.com/boto/boto3/issues/4367
SIGNATURE_V4_BOTO_IGNORED_PARAMS = [
"if-none-match",
"if-match",
]

# headers to blacklist from request_dict.signed_headers
BLACKLISTED_HEADERS = ["X-Amz-Security-Token"]

Expand Down Expand Up @@ -645,7 +653,10 @@ def _get_signed_headers_and_filtered_query_string(
qs_param_low = qs_parameter.lower()
if (
qs_parameter not in SIGNATURE_V4_PARAMS
and qs_param_low.startswith("x-amz-")
and (
qs_param_low.startswith("x-amz-")
or qs_param_low in SIGNATURE_V4_BOTO_IGNORED_PARAMS
)
and qs_param_low not in headers
):
if qs_param_low in signed_headers:
Expand Down
82 changes: 82 additions & 0 deletions tests/aws/services/s3/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from zoneinfo import ZoneInfo

import boto3 as boto3
import botocore
import pytest
import requests
import xmltodict
Expand Down Expand Up @@ -7434,6 +7435,87 @@ def add_content_sha_header(request, **kwargs):
resp = requests.put(rewritten_url, data="something", verify=False)
assert resp.status_code == 403

@markers.aws.validated
def test_pre_signed_url_if_none_match(self, s3_bucket, aws_client, aws_session):
# there currently is a bug in Boto3: https://github.com/boto/boto3/issues/4367
# so we need to use botocore directly to allow testing of this, as other SDK like the Java SDK have the correct
# behavior
object_key = "temp.txt"

s3_endpoint_path_style = _endpoint_url()

# assert that the regular Boto3 client does not work, and does not sign the parameter as requested
client = _s3_client_pre_signed_client(
Config(signature_version="s3v4", s3={}),
endpoint_url=s3_endpoint_path_style,
)
bad_url = client.generate_presigned_url(
"put_object",
Params={"Bucket": s3_bucket, "Key": object_key, "IfNoneMatch": "*"},
)
assert "if-none-match=%2a" not in bad_url.lower()

req = botocore.awsrequest.AWSRequest(
method="PUT",
url=f"{s3_endpoint_path_style}/{s3_bucket}/{object_key}",
data={},
params={
"If-None-Match": "*",
},
headers={},
)

botocore.auth.S3SigV4QueryAuth(aws_session.get_credentials(), "s3", "us-east-1").add_auth(
req
)

assert "if-none-match=%2a" in req.url.lower()

response = requests.put(req.url)
assert response.status_code == 200

response = requests.put(req.url)
# we are now failing because the object already exists
assert response.status_code == 412

@markers.aws.validated
def test_pre_signed_url_if_match(self, s3_bucket, aws_client, aws_session):
key = "test-precondition"
aws_client.s3.put_object(Bucket=s3_bucket, Key=key, Body="test")

s3_endpoint_path_style = _endpoint_url()
# empty object ETag is provided
empty_object_etag = "d41d8cd98f00b204e9800998ecf8427e"

# assert that the regular Boto3 client does not work, and does not sign the parameter as requested
client = _s3_client_pre_signed_client(
Config(signature_version="s3v4", s3={}),
endpoint_url=s3_endpoint_path_style,
)
bad_url = client.generate_presigned_url(
"put_object",
Params={"Bucket": s3_bucket, "Key": key, "IfMatch": empty_object_etag},
)
assert "if-match=d41d8cd98f00b204e9800998ecf8427e" not in bad_url.lower()

req = botocore.awsrequest.AWSRequest(
method="PUT",
url=f"{s3_endpoint_path_style}/{s3_bucket}/{key}",
data={},
params={
"If-Match": empty_object_etag,
},
headers={},
)

botocore.auth.S3SigV4QueryAuth(aws_session.get_credentials(), "s3", "us-east-1").add_auth(
req
)
assert "if-match=d41d8cd98f00b204e9800998ecf8427e" in req.url.lower()

response = requests.put(req.url)
assert response.status_code == 412


class TestS3DeepArchive:
"""
Expand Down
6 changes: 6 additions & 0 deletions tests/aws/services/s3/test_s3.validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,12 @@
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_forward_slash_bucket": {
"last_validated_date": "2025-01-21T18:25:38+00:00"
},
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_if_match": {
"last_validated_date": "2025-05-15T13:08:44+00:00"
},
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_if_none_match": {
"last_validated_date": "2025-05-15T12:51:09+00:00"
},
"tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presign_with_additional_query_params": {
"last_validated_date": "2025-01-21T18:22:43+00:00"
},
Expand Down
Loading