Skip to content

Commit 08c4166

Browse files
committed
Change default S3 presigned URL signature from SigV2 to SigV4
SigV2 silently ignores conditional headers like IfNoneMatch and IfMatch, creating security issues. SigV4 supports header signing and is the modern AWS standard. Breaking change: Users requiring SigV2 must explicitly set signature_version='s3' in client config.
1 parent 04c722b commit 08c4166

3 files changed

Lines changed: 77 additions & 19 deletions

File tree

botocore/client.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def _set_s3_presign_signature_version(
457457
# the customer hasn't set a signature version so we default the
458458
# signature version to sigv2.
459459
client_meta.events.register(
460-
'choose-signer.s3', self._default_s3_presign_to_sigv2
460+
'choose-signer.s3', self._default_s3_presign_to_sigv4
461461
)
462462

463463
def _inject_s3_input_parameters(self, params, context, **kwargs):
@@ -469,11 +469,11 @@ def _inject_s3_input_parameters(self, params, context, **kwargs):
469469
inject_parameter
470470
]
471471

472-
def _default_s3_presign_to_sigv2(self, signature_version, **kwargs):
472+
def _default_s3_presign_to_sigv4(self, signature_version, **kwargs):
473473
"""
474-
Returns the 's3' (sigv2) signer if presigning an s3 request. This is
475-
intended to be used to set the default signature version for the signer
476-
to sigv2. Situations where an asymmetric signature is required are the
474+
Returns the 's3v4' (sigv4) signer if presigning an s3 request. This is
475+
intended to be used to set the default signature version for the signer to sigv4.
476+
Situations where an asymmetric signature is required are the
477477
exception, for example MRAP needs v4a.
478478
479479
:type signature_version: str
@@ -482,7 +482,7 @@ def _default_s3_presign_to_sigv2(self, signature_version, **kwargs):
482482
:type signing_name: str
483483
:param signing_name: The signing name of the service.
484484
485-
:return: 's3' if the request is an s3 presign request, None otherwise
485+
:return: 's3v4' if the request is an s3 presign request, None otherwise
486486
"""
487487
if signature_version.startswith('v4a'):
488488
return
@@ -492,7 +492,7 @@ def _default_s3_presign_to_sigv2(self, signature_version, **kwargs):
492492

493493
for suffix in ['-query', '-presign-post']:
494494
if signature_version.endswith(suffix):
495-
return f's3{suffix}'
495+
return f's3v4{suffix}'
496496

497497
def _register_importexport_events(
498498
self,

tests/functional/test_s3.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,6 +2098,16 @@ def assert_is_v2_presigned_url(self, url):
20982098
self.assertNotIn("X-Amz-Algorithm", qs_components)
20992099
self.assertIn("Signature", qs_components)
21002100

2101+
def assert_is_v4_presigned_url(self, url):
2102+
qs_components = parse_qs(urlsplit(url).query)
2103+
# Assert that it looks like a v4 presigned url by asserting it has
2104+
# the v4 qs components.
2105+
self.assertIn("X-Amz-Credential", qs_components)
2106+
self.assertIn("X-Amz-Algorithm", qs_components)
2107+
self.assertEqual(
2108+
qs_components["X-Amz-Algorithm"], ["AWS4-HMAC-SHA256"]
2109+
)
2110+
21012111
def test_generate_unauthed_url(self):
21022112
config = Config(signature_version=botocore.UNSIGNED)
21032113
client = self.session.create_client("s3", self.region, config=config)
@@ -2116,9 +2126,9 @@ def test_generate_unauthed_post(self):
21162126
}
21172127
self.assertEqual(parts, expected)
21182128

2119-
def test_default_presign_uses_sigv2(self):
2129+
def test_default_presign_uses_sigv4(self):
21202130
url = self.client.generate_presigned_url(ClientMethod="list_buckets")
2121-
self.assertNotIn("Algorithm=AWS4-HMAC-SHA256", url)
2131+
self.assertIn("Algorithm=AWS4-HMAC-SHA256", url)
21222132

21232133
def test_sigv4_presign(self):
21242134
config = Config(signature_version="s3v4")
@@ -2210,43 +2220,43 @@ def test_presign_post_s3_accelerate(self):
22102220
}
22112221
self.assertEqual(parts, expected)
22122222

2213-
def test_presign_uses_v2_for_aws_global(self):
2223+
def test_presign_uses_v4_for_aws_global(self):
22142224
client = self.session.create_client("s3", "aws-global")
22152225
url = client.generate_presigned_url(
22162226
"get_object", {"Bucket": "mybucket", "Key": "mykey"}
22172227
)
2218-
self.assert_is_v2_presigned_url(url)
2228+
self.assert_is_v4_presigned_url(url)
22192229

2220-
def test_presign_uses_v2_for_default_region_with_us_east_1_regional(self):
2230+
def test_presign_uses_v4_for_default_region_with_us_east_1_regional(self):
22212231
config = Config(s3={"us_east_1_regional_endpoint": "regional"})
22222232
client = self.session.create_client("s3", config=config)
22232233
url = client.generate_presigned_url(
22242234
"get_object", {"Bucket": "mybucket", "Key": "mykey"}
22252235
)
2226-
self.assert_is_v2_presigned_url(url)
2236+
self.assert_is_v4_presigned_url(url)
22272237

2228-
def test_presign_uses_v2_for_aws_global_with_us_east_1_regional(self):
2238+
def test_presign_uses_v4_for_aws_global_with_us_east_1_regional(self):
22292239
config = Config(s3={"us_east_1_regional_endpoint": "regional"})
22302240
client = self.session.create_client("s3", "aws-global", config=config)
22312241
url = client.generate_presigned_url(
22322242
"get_object", {"Bucket": "mybucket", "Key": "mykey"}
22332243
)
2234-
self.assert_is_v2_presigned_url(url)
2244+
self.assert_is_v4_presigned_url(url)
22352245

2236-
def test_presign_uses_v2_for_us_east_1(self):
2246+
def test_presign_uses_v4_for_us_east_1(self):
22372247
client = self.session.create_client("s3", "us-east-1")
22382248
url = client.generate_presigned_url(
22392249
"get_object", {"Bucket": "mybucket", "Key": "mykey"}
22402250
)
2241-
self.assert_is_v2_presigned_url(url)
2251+
self.assert_is_v4_presigned_url(url)
22422252

2243-
def test_presign_uses_v2_for_us_east_1_with_us_east_1_regional(self):
2253+
def test_presign_uses_v4_for_us_east_1_with_us_east_1_regional(self):
22442254
config = Config(s3={"us_east_1_regional_endpoint": "regional"})
22452255
client = self.session.create_client("s3", "us-east-1", config=config)
22462256
url = client.generate_presigned_url(
22472257
"get_object", {"Bucket": "mybucket", "Key": "mykey"}
22482258
)
2249-
self.assert_is_v2_presigned_url(url)
2259+
self.assert_is_v4_presigned_url(url)
22502260

22512261

22522262
CHECKSUM_TEST_CASES = [

tests/unit/test_client.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,54 @@ def test_validaties_retry_mode(self):
19851985
botocore.config.Config(retries={'mode': 'turbo-mode'})
19861986

19871987

1988+
class TestS3PresignSignatureVersion(unittest.TestCase):
1989+
"""Test that S3 presigned URLs default to sigv4 instead of sigv2"""
1990+
1991+
def setUp(self):
1992+
self.client_creator = client.ClientCreator(
1993+
loader=mock.Mock(),
1994+
endpoint_resolver=mock.Mock(),
1995+
user_agent='test-agent',
1996+
event_emitter=mock.Mock(),
1997+
retry_handler_factory=mock.Mock(),
1998+
retry_config_translator=mock.Mock(),
1999+
response_parser_factory=mock.Mock(),
2000+
exceptions_factory=mock.Mock(),
2001+
config_store=mock.Mock(),
2002+
user_agent_creator=mock.Mock(),
2003+
)
2004+
2005+
def test_default_s3_presign_returns_s3v4_query(self):
2006+
result = self.client_creator._default_s3_presign_to_sigv4(
2007+
signature_version='v4-query'
2008+
)
2009+
self.assertEqual(result, 's3v4-query')
2010+
2011+
def test_default_s3_presign_returns_s3v4_presign_post(self):
2012+
result = self.client_creator._default_s3_presign_to_sigv4(
2013+
signature_version='v4-presign-post'
2014+
)
2015+
self.assertEqual(result, 's3v4-presign-post')
2016+
2017+
def test_default_s3_presign_ignores_v4a(self):
2018+
result = self.client_creator._default_s3_presign_to_sigv4(
2019+
signature_version='v4a-query'
2020+
)
2021+
self.assertIsNone(result)
2022+
2023+
def test_default_s3_presign_preserves_s3express(self):
2024+
result = self.client_creator._default_s3_presign_to_sigv4(
2025+
signature_version='v4-s3express-query'
2026+
)
2027+
self.assertEqual(result, 'v4-s3express-query')
2028+
2029+
def test_default_s3_presign_ignores_non_presign_signatures(self):
2030+
result = self.client_creator._default_s3_presign_to_sigv4(
2031+
signature_version='v4'
2032+
)
2033+
self.assertIsNone(result)
2034+
2035+
19882036
class TestClientEndpointBridge(unittest.TestCase):
19892037
def setUp(self):
19902038
self.resolver = mock.Mock()

0 commit comments

Comments
 (0)