diff options
author | David Lord <davidism@gmail.com> | 2023-05-01 07:26:37 -0700 |
---|---|---|
committer | David Lord <davidism@gmail.com> | 2023-05-01 07:26:37 -0700 |
commit | db025c61e752b4d422af115afc2ad1497601e024 (patch) | |
tree | ce72963cf1c2dfe09b21c7c7310f93afc825658a | |
parent | c0ab71c4a371697cca2d35f2f7511785cdd16c1f (diff) | |
parent | 07c27a803b0902ec114bf809bee26824ffd82ec5 (diff) | |
download | werkzeug-db025c61e752b4d422af115afc2ad1497601e024.tar.gz |
Merge branch '2.3.x'
-rw-r--r-- | CHANGES.rst | 13 | ||||
-rw-r--r-- | src/werkzeug/__init__.py | 2 | ||||
-rw-r--r-- | src/werkzeug/http.py | 2 | ||||
-rw-r--r-- | src/werkzeug/sansio/multipart.py | 38 | ||||
-rw-r--r-- | src/werkzeug/sansio/response.py | 4 | ||||
-rw-r--r-- | src/werkzeug/test.py | 32 | ||||
-rw-r--r-- | tests/test_test.py | 19 | ||||
-rw-r--r-- | tests/test_wrappers.py | 2 |
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=/", ), ] |