diff --git a/CHANGES.rst b/CHANGES.rst index a749a2129..3040dd45f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py index 203ccf154..6ed4d3024 100644 --- a/src/werkzeug/_internal.py +++ b/src/werkzeug/_internal.py @@ -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: @@ -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) diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py index 72dc3f3eb..07d1fd496 100644 --- a/src/werkzeug/http.py +++ b/src/werkzeug/http.py @@ -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: @@ -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") @@ -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 diff --git a/tests/test_http.py b/tests/test_http.py index 2c6c0b29a..bbd51ba33 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -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