Repository: pallets/werkzeug (python)
Issue: `set_cookie` will no longer set path by default
It seems that since version 2.3.0 of Werkzeug the behaviour of `set_cookie` has changed. The documentation of the method says that by default the cookie will span the whole domain. In older versions this was true: if the parameter `path` was not set it defaulted to `"/"`. Now it defaults to `None`, which leads to `Path` being omitted in the `Set-Cookie` part of the header in the response sent to the client. The browser (tested with Firefox) seems then to just take the path from the request URL.

```
# return from an endpoint like /cookie/set
response.set_cookie('test_cookie', cookie_payload, expires=exp_in_thirty_minutes, secure=True,
                        samesite='Strict')
# --> cookie for client will have path set to /cookie
```

Expected is that `Path` of the cookie will be set to "/" according to the documentation of `set_cookie`.

Environment:

- Python version: 3.10
- Werkzeug version: 2.3.1

Pull request description: cookie sets `Path=/` by default again
If the path parameter is not present, RFC 6265 has a rule for picking a `default-path` that's the parent of the current path. For some reason, Firefox and Chrome on my computer always pick `/` instead, which is why I thought it was ok to remove. This PR restores the previous behavior. Setting `Path=/` is also a requirement for the `__Host-` and `__Secure-` [cookie prefixes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes).

fixes #2672 

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index e47232fe..7dd5f1ba 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,8 @@ Unreleased
 -   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/http.py b/src/werkzeug/http.py
index b408c624..a2075c70 100644
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -1341,7 +1341,7 @@ def dump_cookie(
     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,
@@ -1388,6 +1388,9 @@ def dump_cookie(
 
     .. _`cookie`: http://browsercookielimits.squawky.net/
 
+    .. versionchanged:: 2.3.1
+        The ``path`` parameter is ``/`` by default.
+
     .. versionchanged:: 2.3
         ``localhost`` and other names without a dot are allowed for the domain. A
         leading dot is ignored.


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
index e82f869d..5ceb7ef5 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -452,10 +452,11 @@ class TestHTTPUtility:
         assert set(rv.split("; ")) == {
             "HttpOnly",
             "Max-Age=360",
+            "Path=/",
             'foo="bar baz blub"',
         }
-        assert http.dump_cookie("key", "xxx/") == "key=xxx/"
-        assert http.dump_cookie("key", "xxx=") == "key=xxx="
+        assert http.dump_cookie("key", "xxx/") == "key=xxx/; Path=/"
+        assert http.dump_cookie("key", "xxx=", path=None) == "key=xxx="
 
     def test_bad_cookies(self):
         cookies = http.parse_cookie(
@@ -478,7 +479,7 @@ class TestHTTPUtility:
 
     def test_cookie_quoting(self):
         val = http.dump_cookie("foo", "?foo")
-        assert val == "foo=?foo"
+        assert val == "foo=?foo; Path=/"
         assert http.parse_cookie(val)["foo"] == "?foo"
         assert http.parse_cookie(r'foo="foo\054bar"')["foo"] == "foo,bar"
 
@@ -488,13 +489,13 @@ class TestHTTPUtility:
 
     def test_cookie_domain_resolving(self):
         val = http.dump_cookie("foo", "bar", domain="\N{SNOWMAN}.com")
-        assert val == "foo=bar; Domain=xn--n3h.com"
+        assert val == "foo=bar; Domain=xn--n3h.com; Path=/"
 
     def test_cookie_unicode_dumping(self):
         val = http.dump_cookie("foo", "\N{SNOWMAN}")
         h = datastructures.Headers()
         h.add("Set-Cookie", val)
-        assert h["Set-Cookie"] == 'foo="\\342\\230\\203"'
+        assert h["Set-Cookie"] == 'foo="\\342\\230\\203"; Path=/'
 
         cookies = http.parse_cookie(h["Set-Cookie"])
         assert cookies["foo"] == "\N{SNOWMAN}"
@@ -502,7 +503,7 @@ class TestHTTPUtility:
     def test_cookie_unicode_keys(self):
         # Yes, this is technically against the spec but happens
         val = http.dump_cookie("fö", "fö")
-        assert val == _wsgi_encoding_dance('fö="f\\303\\266"', "utf-8")
+        assert val == _wsgi_encoding_dance('fö="f\\303\\266"; Path=/')
         cookies = http.parse_cookie(val)
         assert cookies["fö"] == "fö"
 
@@ -513,20 +514,20 @@ class TestHTTPUtility:
 
     def test_cookie_domain_encoding(self):
         val = http.dump_cookie("foo", "bar", domain="\N{SNOWMAN}.com")
-        assert val == "foo=bar; Domain=xn--n3h.com"
+        assert val == "foo=bar; Domain=xn--n3h.com; Path=/"
 
         val = http.dump_cookie("foo", "bar", domain="foo.com")
-        assert val == "foo=bar; Domain=foo.com"
+        assert val == "foo=bar; Domain=foo.com; Path=/"
 
     def test_cookie_maxsize(self):
-        val = http.dump_cookie("foo", "bar" * 1363)
+        val = http.dump_cookie("foo", "bar" * 1360 + "b")
         assert len(val) == 4093
 
         with pytest.warns(UserWarning, match="cookie is too large"):
-            http.dump_cookie("foo", "bar" * 1364)
+            http.dump_cookie("foo", "bar" * 1360 + "ba")
 
         with pytest.warns(UserWarning, match="the limit is 512 bytes"):
-            http.dump_cookie("foo", "w" * 509, max_size=512)
+            http.dump_cookie("foo", "w" * 501, max_size=512)
 
     @pytest.mark.parametrize(
         ("samesite", "expected"),
@@ -538,7 +539,7 @@ class TestHTTPUtility:
         ),
     )
     def test_cookie_samesite_attribute(self, samesite, expected):
-        value = http.dump_cookie("foo", "bar", samesite=samesite)
+        value = http.dump_cookie("foo", "bar", samesite=samesite, path=None)
         assert value == expected
 
     def test_cookie_samesite_invalid(self):