|
92 | 92 | _UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n'] |
93 | 93 |
|
94 | 94 | # Allowed valid characters in parse_qsl |
95 | | -_VALID_QUERY_CHARS = re.compile(r"^[A-Za-z0-9\-._~!$&'()*+,;=:@/?%]*$") |
| 95 | +_VALID_QUERY_CHARS = "-._~!$&'()*+,;=:@/?%" |
96 | 96 |
|
97 | 97 | def clear_cache(): |
98 | 98 | """Clear internal performance caches. Undocumented; some tests want it.""" |
@@ -781,6 +781,15 @@ def parse_qs(qs, keep_blank_values=False, strict_parsing=False, |
781 | 781 | parsed_result[name] = [value] |
782 | 782 | return parsed_result |
783 | 783 |
|
| 784 | +def _is_valid_query(to_check: str) -> bool: |
| 785 | + """Return True if all characters are valid per RFC 3986.""" |
| 786 | + for ch in to_check: |
| 787 | + if not ch.isascii(): |
| 788 | + return False |
| 789 | + if ch.isalnum() or ch in _VALID_QUERY_CHARS: |
| 790 | + continue |
| 791 | + return False |
| 792 | + return True |
784 | 793 |
|
785 | 794 | def parse_qsl(qs, keep_blank_values=False, strict_parsing=False, |
786 | 795 | encoding='utf-8', errors='replace', max_num_fields=None, separator='&', *, _stacklevel=1): |
@@ -860,7 +869,7 @@ def _unquote(s): |
860 | 869 | if strict_parsing: |
861 | 870 | # Validate RFC3986 characters |
862 | 871 | to_check = (name_value.decode() if isinstance(name_value, bytes) else name_value) |
863 | | - if not _VALID_QUERY_CHARS.match(to_check): |
| 872 | + if not _is_valid_query(to_check): |
864 | 873 | raise ValueError(f"Invalid characters in query string per RFC 3986: {name_value!r}") |
865 | 874 | if value or keep_blank_values: |
866 | 875 | name = _unquote(name) |
|
0 commit comments