Skip to content

Commit

Permalink
Fix qvalue parsing (#2753)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism authored Aug 13, 2023
2 parents dd1f137 + 88f4ed6 commit ac9974c
Show file tree
Hide file tree
Showing 4 changed files with 14 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Unreleased
enforcement. :issue:`2734`
- Fix empty file streaming when testing. :issue:`2740`
- Clearer error message when URL rule does not start with slash. :pr:`2750`
- ``Accept`` ``q`` value can be a float without a decimal part. :issue:`2751`


Version 2.3.6
Expand Down
17 changes: 0 additions & 17 deletions src/werkzeug/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ def _decode_idna(domain: str) -> str:


_plain_int_re = re.compile(r"-?\d+", re.ASCII)
_plain_float_re = re.compile(r"-?\d+\.\d+", re.ASCII)


def _plain_int(value: str) -> int:
Expand All @@ -329,19 +328,3 @@ def _plain_int(value: str) -> int:
raise ValueError

return int(value)


def _plain_float(value: str) -> float:
"""Parse a float only if it is only ASCII digits and ``-``, and contains digits
before and after the ``.``.
This disallows ``+``, ``_``, non-ASCII digits, and ``.123``, which are accepted by
``float`` but are not allowed in HTTP header values.
Any leading or trailing whitespace is stripped
"""
value = value.strip()
if _plain_float_re.fullmatch(value) is None:
raise ValueError

return float(value)
12 changes: 7 additions & 5 deletions src/werkzeug/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from urllib.request import parse_http_list as _parse_list_header

from ._internal import _dt_as_utc
from ._internal import _plain_float
from ._internal import _plain_int

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -614,6 +613,7 @@ def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:
return value, options


_q_value_re = re.compile(r"-?\d+(\.\d+)?", re.ASCII)
_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")


Expand Down Expand Up @@ -656,13 +656,15 @@ def parse_accept_header(
item, options = parse_options_header(item)

if "q" in options:
try:
# pop q, remaining options are reconstructed
q = _plain_float(options.pop("q"))
except ValueError:
# pop q, remaining options are reconstructed
q_str = options.pop("q").strip()

if _q_value_re.fullmatch(q_str) is None:
# ignore an invalid q
continue

q = float(q_str)

if q < 0 or q > 1:
# ignore an invalid q
continue
Expand Down
6 changes: 6 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,12 @@ def test_accept_invalid_float(value):
assert list(a.values()) == ["en"]


def test_accept_valid_int_one_zero():
assert http.parse_accept_header("en;q=1") == http.parse_accept_header("en;q=1.0")
assert http.parse_accept_header("en;q=0") == http.parse_accept_header("en;q=0.0")
assert http.parse_accept_header("en;q=5") == http.parse_accept_header("en;q=5.0")


@pytest.mark.parametrize("value", ["🯱🯲🯳", "+1-", "1-1_23"])
def test_range_invalid_int(value):
assert http.parse_range_header(value) is None
Expand Down

0 comments on commit ac9974c

Please sign in to comment.