Skip to content

Commit

Permalink
fail on Python's extended int/float syntax (#2723)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism authored Jun 7, 2023
2 parents 1892c10 + 6290332 commit 86c5c78
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 10 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Unreleased
encoding. :issue:`2700`
- ``iri_to_uri`` shows a deprecation warning instead of an error when passing bytes.
:issue:`2708`
- When parsing numbers in HTTP request headers such as ``Content-Length``, only ASCII
digits are accepted rather than any format that Python's ``int`` and ``float``
accept. :issue:`2716`


Version 2.3.4
Expand Down
30 changes: 30 additions & 0 deletions src/werkzeug/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import operator
import re
import sys
import typing as t
from datetime import datetime
Expand Down Expand Up @@ -309,3 +310,32 @@ def _decode_idna(domain: str) -> str:
parts.append(part.decode("ascii"))

return ".".join(parts)


_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:
"""Parse an int only if it is only ASCII digits and ``-``.
This disallows ``+``, ``_``, and non-ASCII digits, which are accepted by ``int`` but
are not allowed in HTTP header values.
"""
if _plain_int_re.fullmatch(value) is None:
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.
"""
if _plain_float_re.fullmatch(value) is None:
raise ValueError

return float(value)
3 changes: 2 additions & 1 deletion src/werkzeug/datastructures/file_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from os import fsdecode
from os import fspath

from .._internal import _plain_int
from .structures import MultiDict


Expand Down Expand Up @@ -67,7 +68,7 @@ def content_type(self):
def content_length(self):
"""The content-length sent in the header. Usually not available"""
try:
return int(self.headers.get("content-length") or 0)
return _plain_int(self.headers.get("content-length") or 0)
except ValueError:
return 0

Expand Down
3 changes: 2 additions & 1 deletion src/werkzeug/formparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from io import BytesIO
from urllib.parse import parse_qsl

from ._internal import _plain_int
from .datastructures import FileStorage
from .datastructures import Headers
from .datastructures import MultiDict
Expand Down Expand Up @@ -465,7 +466,7 @@ def start_file_streaming(
content_type = event.headers.get("content-type")

try:
content_length = int(event.headers["content-length"])
content_length = _plain_int(event.headers["content-length"])
except (KeyError, ValueError):
content_length = 0

Expand Down
16 changes: 9 additions & 7 deletions src/werkzeug/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
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:
from _typeshed.wsgi import WSGIEnvironment
Expand Down Expand Up @@ -656,7 +658,7 @@ def parse_accept_header(
if "q" in options:
try:
# pop q, remaining options are reconstructed
q = float(options.pop("q"))
q = _plain_float(options.pop("q"))
except ValueError:
# ignore an invalid q
continue
Expand Down Expand Up @@ -914,7 +916,7 @@ def parse_range_header(
if last_end < 0:
return None
try:
begin = int(item)
begin = _plain_int(item)
except ValueError:
return None
end = None
Expand All @@ -925,15 +927,15 @@ def parse_range_header(
end_str = end_str.strip()

try:
begin = int(begin_str)
begin = _plain_int(begin_str)
except ValueError:
return None

if begin < last_end or last_end < 0:
return None
if end_str:
try:
end = int(end_str) + 1
end = _plain_int(end_str) + 1
except ValueError:
return None

Expand Down Expand Up @@ -976,7 +978,7 @@ def parse_content_range_header(
length = None
else:
try:
length = int(length_str)
length = _plain_int(length_str)
except ValueError:
return None

Expand All @@ -990,8 +992,8 @@ def parse_content_range_header(

start_str, stop_str = rng.split("-", 1)
try:
start = int(start_str)
stop = int(stop_str) + 1
start = _plain_int(start_str)
stop = _plain_int(stop_str) + 1
except ValueError:
return None

Expand Down
3 changes: 2 additions & 1 deletion src/werkzeug/sansio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typing as t
from urllib.parse import quote

from .._internal import _plain_int
from ..exceptions import SecurityError
from ..urls import uri_to_iri

Expand Down Expand Up @@ -153,6 +154,6 @@ def get_content_length(
return None

try:
return max(0, int(http_content_length))
return max(0, _plain_int(http_content_length))
except ValueError:
return 0
4 changes: 4 additions & 0 deletions tests/sansio/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
(Headers({"Transfer-Encoding": "chunked", "Content-Length": "6"}), None),
(Headers({"Transfer-Encoding": "something", "Content-Length": "6"}), 6),
(Headers({"Content-Length": "6"}), 6),
(Headers({"Content-Length": "-6"}), 0),
(Headers({"Content-Length": "+123"}), 0),
(Headers({"Content-Length": "1_23"}), 0),
(Headers({"Content-Length": "🯱🯲🯳"}), 0),
(Headers(), None),
],
)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import urllib.parse
from datetime import date
from datetime import datetime
from datetime import timedelta
Expand Down Expand Up @@ -760,3 +761,26 @@ def test_parse_date(value, expect):
)
def test_http_date(value, expect):
assert http.http_date(value) == expect


@pytest.mark.parametrize("value", [".5", "+0.5", "0.5_1", "🯰.🯵"])
def test_accept_invalid_float(value):
quoted = urllib.parse.quote(value)

if quoted == value:
q = f"q={value}"
else:
q = f"q*=UTF-8''{value}"

a = http.parse_accept_header(f"en,jp;{q}")
assert list(a.values()) == ["en"]


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


@pytest.mark.parametrize("value", ["*/🯱🯲🯳", "1-+2/3", "1_23-125/*"])
def test_content_range_invalid_int(value):
assert http.parse_content_range_header(f"bytes {value}") is None

0 comments on commit 86c5c78

Please sign in to comment.