Skip to content

Commit ccc97a1

Browse files
committed
Enforce RFC 3986 query chars in strict parse_qsl
1 parent 0fff6bd commit ccc97a1

3 files changed

Lines changed: 34 additions & 0 deletions

File tree

Lib/test/test_urlparse.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,18 @@ def test_parse_qsl_errors(self):
15411541
with self.assertRaises(UnicodeDecodeError):
15421542
urllib.parse.parse_qsl('a=b', separator=b'\xa6')
15431543

1544+
def test_parse_qsl_strict_invalid_query_chars(self):
1545+
# gh-135523: RFC 3986 excludes '#', '[', ']', etc. from query
1546+
for qs in ['foo=#', 'foo=[bar]', 'a={b}', 'a=b c']:
1547+
with self.subTest(qs=qs):
1548+
self.assertRaises(ValueError, urllib.parse.parse_qsl,
1549+
qs, strict_parsing=True)
1550+
# bytes input
1551+
self.assertRaises(ValueError, urllib.parse.parse_qsl,
1552+
b'foo=#', strict_parsing=True)
1553+
# valid query chars accepted
1554+
urllib.parse.parse_qsl('a=1&b=2%20x', strict_parsing=True)
1555+
15441556
def test_urlencode_sequences(self):
15451557
# Other tests incidentally urlencode things; test non-covered cases:
15461558
# Sequence and object values.

Lib/urllib/parse.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,13 @@ def unquote(string, encoding='utf-8', errors='replace'):
856856
return ''.join(_generate_unquoted_parts(string, encoding, errors))
857857

858858

859+
# RFC 3986
860+
_VALID_QUERY_CHARS = frozenset(
861+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
862+
"-._~!$&'()*+,;=:@/?%"
863+
)
864+
865+
859866
def parse_qs(qs, keep_blank_values=False, strict_parsing=False,
860867
encoding='utf-8', errors='replace', max_num_fields=None, separator='&'):
861868
"""Parse a query given as a string argument.
@@ -968,6 +975,19 @@ def _unquote(s):
968975
if max_num_fields < num_fields:
969976
raise ValueError('Max number of fields exceeded')
970977

978+
if strict_parsing:
979+
if isinstance(qs, bytes):
980+
qs_chars = {chr(b) for b in qs}
981+
sep_chars = {chr(b) for b in separator}
982+
else:
983+
qs_chars = set(qs)
984+
sep_chars = set(separator)
985+
invalid = qs_chars - _VALID_QUERY_CHARS - sep_chars
986+
if invalid:
987+
raise ValueError(
988+
"invalid query characters: %r" % ''.join(sorted(invalid))
989+
)
990+
971991
r = []
972992
for name_value in qs.split(separator):
973993
if name_value or strict_parsing:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`urllib.parse.parse_qsl` and :func:`urllib.parse.parse_qs` now reject
2+
characters not valid per :rfc:`3986` when *strict_parsing* is ``True``.

0 commit comments

Comments
 (0)