Skip to content

Commit 638e5df

Browse files
authored
fix: Fixed double decoding issue with unquote() (#4222)
1 parent 88d16d0 commit 638e5df

4 files changed

Lines changed: 38 additions & 2 deletions

File tree

redis/asyncio/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1749,7 +1749,10 @@ def parse_url(url: str) -> ConnectKwargs:
17491749

17501750
for name, value_list in parse_qs(parsed.query).items():
17511751
if value_list and len(value_list) > 0:
1752-
value = unquote(value_list[0])
1752+
# parse_qs() already percent-decodes query values, so use the value
1753+
# as-is; unquoting again here would double-decode (e.g. "%2520" ->
1754+
# "%20" -> " "). See issue #4208.
1755+
value = value_list[0]
17531756
parser = URL_QUERY_ARGUMENT_PARSERS.get(name)
17541757
if parser:
17551758
try:

redis/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2291,7 +2291,10 @@ def parse_url(url):
22912291

22922292
for name, value in parse_qs(url.query).items():
22932293
if value and len(value) > 0:
2294-
value = unquote(value[0])
2294+
# parse_qs() already percent-decodes query values, so use the value
2295+
# as-is; unquoting again here would double-decode (e.g. "%2520" ->
2296+
# "%20" -> " "). See issue #4208.
2297+
value = value[0]
22952298
parser = URL_QUERY_ARGUMENT_PARSERS.get(name)
22962299
if parser:
22972300
try:

tests/test_asyncio/test_connection_pool.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,21 @@ def test_client_name_in_querystring(self):
617617
pool = redis.ConnectionPool.from_url("redis://location?client_name=test-client")
618618
assert pool.connection_kwargs["client_name"] == "test-client"
619619

620+
def test_querystring_value_percent_decoded_once(self):
621+
# A single percent-encoded sequence is decoded exactly once.
622+
pool = redis.ConnectionPool.from_url(
623+
"redis://location?client_name=worker%20name"
624+
)
625+
assert pool.connection_kwargs["client_name"] == "worker name"
626+
627+
def test_querystring_value_not_double_percent_decoded(self):
628+
# A literal percent sign in a query value (encoded as %2520) must survive
629+
# as "%20" rather than being decoded a second time into a space. See #4208.
630+
pool = redis.ConnectionPool.from_url(
631+
"redis://location?client_name=worker%2520name"
632+
)
633+
assert pool.connection_kwargs["client_name"] == "worker%20name"
634+
620635
def test_invalid_extra_typed_querystring_options(self):
621636
with pytest.raises(ValueError):
622637
redis.ConnectionPool.from_url(

tests/test_connection_pool.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,21 @@ def test_client_name_in_querystring(self):
515515
pool = redis.ConnectionPool.from_url("redis://location?client_name=test-client")
516516
assert pool.connection_kwargs["client_name"] == "test-client"
517517

518+
def test_querystring_value_percent_decoded_once(self):
519+
# A single percent-encoded sequence is decoded exactly once.
520+
pool = redis.ConnectionPool.from_url(
521+
"redis://location?client_name=worker%20name"
522+
)
523+
assert pool.connection_kwargs["client_name"] == "worker name"
524+
525+
def test_querystring_value_not_double_percent_decoded(self):
526+
# A literal percent sign in a query value (encoded as %2520) must survive
527+
# as "%20" rather than being decoded a second time into a space. See #4208.
528+
pool = redis.ConnectionPool.from_url(
529+
"redis://location?client_name=worker%2520name"
530+
)
531+
assert pool.connection_kwargs["client_name"] == "worker%20name"
532+
518533
def test_invalid_extra_typed_querystring_options(self):
519534
with pytest.raises(ValueError):
520535
redis.ConnectionPool.from_url(

0 commit comments

Comments
 (0)