Skip to content

Commit 68d6412

Browse files
authored
Fix SigV4 signature for URIs with literal query parameters (#674)
* Fix SigV4 signature for URIs with literal query parameters * Add unit tests for _format_canonical_query in SigV4Signer
1 parent b2fad24 commit 68d6412

File tree

3 files changed

+30
-2
lines changed

3 files changed

+30
-2
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "bugfix",
3+
"description": "Fixed SigV4 signature computation for URIs with literal query parameters (e.g., ?sync). parse_qsl was silently dropping query keys without values, causing InvalidSignatureException."
4+
}

packages/aws-sdk-signers/src/aws_sdk_signers/signers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def _format_canonical_query(self, *, query: str | None) -> str:
315315
if query is None:
316316
return ""
317317

318-
query_params = parse_qsl(qs=query)
318+
query_params = parse_qsl(qs=query, keep_blank_values=True)
319319
query_parts = (
320320
(quote(string=key, safe=""), quote(string=value, safe=""))
321321
for key, value in query_params
@@ -695,7 +695,7 @@ async def _format_canonical_query(self, *, query: str | None) -> str:
695695
if query is None:
696696
return ""
697697

698-
query_params = parse_qsl(qs=query)
698+
query_params = parse_qsl(qs=query, keep_blank_values=True)
699699
query_parts = (
700700
(quote(string=key, safe=""), quote(string=value, safe=""))
701701
for key, value in query_params

packages/aws-sdk-signers/tests/unit/test_signers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@ def test_sign_with_expired_identity(
125125
identity=identity,
126126
)
127127

128+
def test_format_canonical_query_keeps_blank_values(self) -> None:
129+
canonical_query = self.SIGV4_SYNC_SIGNER._format_canonical_query( # pyright: ignore[reportPrivateUsage]
130+
query="foo=bar&baz="
131+
)
132+
assert canonical_query == "baz=&foo=bar"
133+
134+
def test_format_canonical_query_with_literal_query_param(self) -> None:
135+
canonical_query = self.SIGV4_SYNC_SIGNER._format_canonical_query( # pyright: ignore[reportPrivateUsage]
136+
query="sync"
137+
)
138+
assert canonical_query == "sync="
139+
128140

129141
class UnreadableAsyncStream:
130142
def __aiter__(self) -> typing.Self:
@@ -231,3 +243,15 @@ async def test_sign_event_stream(
231243
assert "X-Amz-Content-SHA256" in signed.fields
232244
payload_hash = signed.fields["X-Amz-Content-SHA256"].as_string()
233245
assert payload_hash == "STREAMING-AWS4-HMAC-SHA256-EVENTS"
246+
247+
async def test_format_canonical_query_keeps_blank_values(self) -> None:
248+
canonical_query = await self.SIGV4_ASYNC_SIGNER._format_canonical_query( # pyright: ignore[reportPrivateUsage]
249+
query="foo=bar&baz="
250+
)
251+
assert canonical_query == "baz=&foo=bar"
252+
253+
async def test_format_canonical_query_with_literal_query_param(self) -> None:
254+
canonical_query = await self.SIGV4_ASYNC_SIGNER._format_canonical_query( # pyright: ignore[reportPrivateUsage]
255+
query="sync"
256+
)
257+
assert canonical_query == "sync="

0 commit comments

Comments
 (0)