Repository: pallets/werkzeug

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



Discussion:
That's correct. If the path is not given, browsers do not set a limit on the path, therefore it does not need to be sent by default. I can't reproduce your result, Firefox and Chrome do not use the current path, they use `/`.
I have here an minimal example:

```
from werkzeug.wrappers import Request, Response


def set_cookie(request):
    response = Response("Cookie set!")
    response.set_cookie("my_cookie", "cookie_value")
    return response


def application(environ, start_response):
    request = Request(environ)
    response = None

    if request.path == '/cookie/set':
        response = set_cookie(request)
    else:
        response = Response("Page not found", status=404)

    return response(environ, start_response)


if __name__ == '__main__':
    from werkzeug.serving import run_simple

    run_simple('localhost', 5000, application)
```

after visiting `http://localhost:5000/cookie/set` I have the following cookie:

![image](https://user-images.githubusercontent.com/17177211/235173132-86c49c0b-e54d-4de1-a5cb-3b398bb05bd8.png)

If this behavior differs it must be some Browser differences. 
But in my understanding it seems to match the behavior specified here [RFC 6265 ](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4) using the definition of `default-path` from [here](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4).
Again, I can't reproduce your result. Firefox and Chrome both use `/` if the path is not given. I used your example, navigated to `http://localhost:5000/cookie/set`, and the cookie was set with `/` for the path.
OK, that is confusing. I just tried it with Chrome as well (running the minimal example from my last response) and I get the same behavior:

![image](https://user-images.githubusercontent.com/17177211/235176277-63da0d00-2ffe-485d-b6d0-bb81d97dd052.png)

Output from `pip freeze`:

```
MarkupSafe==2.1.2
Werkzeug==2.3.1
```
with Python 3.10.10

Hmm, I originally removed it because it looked like the default was `/` anyway, which saved 8 bytes. But I missed the RFC's description of `default-path`. Maybe my browser's behavior is due to different privacy settings or something? I can add `Path=/` back in.

Resolving patch:
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
--- 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
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -452,10 +452,11 @@ def test_dump_cookie(self):
         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 @@ def test_empty_keys_are_ignored(self):
 
     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 @@ def test_parse_set_cookie_directive(self):
 
     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 @@ def test_cookie_unicode_dumping(self):
     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 @@ def test_cookie_unicode_parsing(self):
 
     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 @@ def test_cookie_maxsize(self):
         ),
     )
     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):