summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2023-05-01 07:26:37 -0700
committerDavid Lord <davidism@gmail.com>2023-05-01 07:26:37 -0700
commitdb025c61e752b4d422af115afc2ad1497601e024 (patch)
treece72963cf1c2dfe09b21c7c7310f93afc825658a
parentc0ab71c4a371697cca2d35f2f7511785cdd16c1f (diff)
parent07c27a803b0902ec114bf809bee26824ffd82ec5 (diff)
downloadwerkzeug-db025c61e752b4d422af115afc2ad1497601e024.tar.gz
Merge branch '2.3.x'
-rw-r--r--CHANGES.rst13
-rw-r--r--src/werkzeug/__init__.py2
-rw-r--r--src/werkzeug/http.py2
-rw-r--r--src/werkzeug/sansio/multipart.py38
-rw-r--r--src/werkzeug/sansio/response.py4
-rw-r--r--src/werkzeug/test.py32
-rw-r--r--tests/test_test.py19
-rw-r--r--tests/test_wrappers.py2
8 files changed, 75 insertions, 37 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 4cd280c2..091aa553 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,16 @@
.. currentmodule:: werkzeug
+Version 2.3.3
+-------------
+
+Unreleased
+
+- Fix parsing of large multipart bodies. Remove invalid leading newline, and restore
+ parsing speed. :issue:`2658, 2675`
+- The cookie ``Path`` attribute is set to ``/`` by default again, to prevent clients
+ from falling back to RFC 6265's ``default-path`` behavior. :issue:`2672, 2679`
+
+
Version 2.3.2
-------------
@@ -8,8 +19,6 @@ Released 2023-04-28
- Parse the cookie ``Expires`` attribute correctly in the test client. :issue:`2669`
- ``max_content_length`` can only be enforced on streaming requests if the server
sets ``wsgi.input_terminated``. :issue:`2668`
-- The cookie ``Path`` attribute is set to ``/`` by default again, to prevent clients
- from falling back to RFC 6265's ``default-path`` behavior. :issue:`2672`
Version 2.3.1
diff --git a/src/werkzeug/__init__.py b/src/werkzeug/__init__.py
index 2dd16022..64640b0e 100644
--- a/src/werkzeug/__init__.py
+++ b/src/werkzeug/__init__.py
@@ -3,4 +3,4 @@ from .test import Client as Client
from .wrappers import Request as Request
from .wrappers import Response as Response
-__version__ = "2.3.2"
+__version__ = "2.3.3.dev"
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
index 7edd1d3b..d38ca9a5 100644
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -1388,7 +1388,7 @@ def dump_cookie(
.. _`cookie`: http://browsercookielimits.squawky.net/
- .. versionchanged:: 2.3.2
+ .. versionchanged:: 2.3.3
The ``path`` parameter is ``/`` by default.
.. versionchanged:: 2.3.1
diff --git a/src/werkzeug/sansio/multipart.py b/src/werkzeug/sansio/multipart.py
index ae633b81..11e65ed0 100644
--- a/src/werkzeug/sansio/multipart.py
+++ b/src/werkzeug/sansio/multipart.py
@@ -121,15 +121,15 @@ class MultipartDecoder:
self._search_position = 0
self._parts_decoded = 0
- def last_newline(self) -> int:
+ def last_newline(self, data: bytes) -> int:
try:
- last_nl = self.buffer.rindex(b"\n")
+ last_nl = data.rindex(b"\n")
except ValueError:
- last_nl = len(self.buffer)
+ last_nl = len(data)
try:
- last_cr = self.buffer.rindex(b"\r")
+ last_cr = data.rindex(b"\r")
except ValueError:
- last_cr = len(self.buffer)
+ last_cr = len(data)
return min(last_nl, last_cr)
@@ -251,17 +251,25 @@ class MultipartDecoder:
else:
data_start = 0
- match = self.boundary_re.search(data)
- if match is not None:
- if match.group(1).startswith(b"--"):
- self.state = State.EPILOGUE
- else:
- self.state = State.PART
- data_end = match.start()
- del_index = match.end()
+ if self.buffer.find(b"--" + self.boundary) == -1:
+ # No complete boundary in the buffer, but there may be
+ # a partial boundary at the end. As the boundary
+ # starts with either a nl or cr find the earliest and
+ # return up to that as data.
+ data_end = del_index = self.last_newline(data[data_start:])
+ more_data = True
else:
- data_end = del_index = self.last_newline()
- more_data = match is None
+ match = self.boundary_re.search(data)
+ if match is not None:
+ if match.group(1).startswith(b"--"):
+ self.state = State.EPILOGUE
+ else:
+ self.state = State.PART
+ data_end = match.start()
+ del_index = match.end()
+ else:
+ data_end = del_index = self.last_newline(data[data_start:])
+ more_data = match is None
return bytes(data[data_start:data_end]), del_index, more_data
diff --git a/src/werkzeug/sansio/response.py b/src/werkzeug/sansio/response.py
index fcbc03ea..387a4ae7 100644
--- a/src/werkzeug/sansio/response.py
+++ b/src/werkzeug/sansio/response.py
@@ -225,7 +225,7 @@ class Response:
value: str = "",
max_age: timedelta | int | None = None,
expires: str | datetime | int | float | None = None,
- path: str | None = None,
+ path: str | None = "/",
domain: str | None = None,
secure: bool = False,
httponly: bool = False,
@@ -276,7 +276,7 @@ class Response:
def delete_cookie(
self,
key: str,
- path: str | None = None,
+ path: str | None = "/",
domain: str | None = None,
secure: bool = False,
httponly: bool = False,
diff --git a/src/werkzeug/test.py b/src/werkzeug/test.py
index 0f61d0ca..012ec30b 100644
--- a/src/werkzeug/test.py
+++ b/src/werkzeug/test.py
@@ -948,7 +948,7 @@ class Client:
value = args[0]
cookie = Cookie._from_response_header(
- domain, dump_cookie(key, value, domain=domain, path=path, **kwargs)
+ domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs)
)
cookie.origin_only = origin_only
@@ -1037,7 +1037,7 @@ class Client:
environ.pop("HTTP_COOKIE", None)
def _update_cookies_from_response(
- self, server_name: str, headers: list[str]
+ self, server_name: str, path: str, headers: list[str]
) -> None:
"""If cookies are enabled, update the stored cookies from any ``Set-Cookie``
headers in the response.
@@ -1050,7 +1050,7 @@ class Client:
return
for header in headers:
- cookie = Cookie._from_response_header(server_name, header)
+ cookie = Cookie._from_response_header(server_name, path, header)
if cookie._should_delete:
self._cookies.pop(cookie._storage_key, None)
@@ -1066,8 +1066,10 @@ class Client:
"""
self._add_cookies_to_wsgi(environ)
rv = run_wsgi_app(self.application, environ, buffered=buffered)
- server_name = urlsplit(get_current_url(environ)).hostname or "localhost"
- self._update_cookies_from_response(server_name, rv[2].getlist("Set-Cookie"))
+ url = urlsplit(get_current_url(environ))
+ self._update_cookies_from_response(
+ url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie")
+ )
return rv
def resolve_redirect(
@@ -1506,7 +1508,7 @@ class Cookie:
return f"{self.key}={self.value}"
@classmethod
- def _from_response_header(cls, server_name: str, header: str) -> te.Self:
+ def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self:
header, _, parameters_str = header.partition(";")
key, _, value = header.partition("=")
decoded_key, decoded_value = next(parse_cookie(header).items())
@@ -1514,21 +1516,21 @@ class Cookie:
for item in parameters_str.split(";"):
k, sep, v = item.partition("=")
- params[k.strip()] = v.strip() if sep else None
+ params[k.strip().lower()] = v.strip() if sep else None
return cls(
key=key.strip(),
value=value.strip(),
decoded_key=decoded_key,
decoded_value=decoded_value,
- expires=parse_date(params.get("Expires")),
- max_age=int(params["Max-Age"] or 0) if "Max-Age" in params else None,
- domain=params.get("Domain", server_name) or server_name,
- origin_only="Domain" not in params,
- path=params.get("Path", "/") or "/",
- secure="Secure" in params,
- http_only="HttpOnly" in params,
- same_site=params.get("SameSite"),
+ expires=parse_date(params.get("expires")),
+ max_age=int(params["max-age"] or 0) if "max-age" in params else None,
+ domain=params.get("domain") or server_name,
+ origin_only="domain" not in params,
+ path=params.get("path") or path.rpartition("/")[0] or "/",
+ secure="secure" in params,
+ http_only="httponly" in params,
+ same_site=params.get("samesite"),
)
@property
diff --git a/tests/test_test.py b/tests/test_test.py
index e3a0fff8..a7390008 100644
--- a/tests/test_test.py
+++ b/tests/test_test.py
@@ -117,6 +117,25 @@ def test_cookie_for_different_path():
assert response.text == "test=test"
+def test_cookie_default_path() -> None:
+ """When no path is set for a cookie, the default uses everything up to but not
+ including the first slash.
+ """
+
+ @Request.application
+ def app(request: Request) -> Response:
+ r = Response()
+ r.set_cookie("k", "v", path=None)
+ return r
+
+ c = Client(app)
+ c.get("/nested/leaf")
+ assert c.get_cookie("k") is None
+ assert c.get_cookie("k", path="/nested") is not None
+ c.get("/nested/dir/")
+ assert c.get_cookie("k", path="/nested/dir") is not None
+
+
def test_environ_builder_basics():
b = EnvironBuilder()
assert b.content_type is None
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
index 2e257cea..8a91aefc 100644
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -271,7 +271,7 @@ def test_base_response():
("Content-Type", "text/plain; charset=utf-8"),
(
"Set-Cookie",
- "foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0",
+ "foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/",
),
]