Repository: pallets/werkzeug

Issue:
cookies with ':', '_' and some other characters are quoted
Commit https://github.com/pallets/werkzeug/commit/cf0102d6443e936f95b21b69e3deb65028d6fe3b#diff-cd95d3a95564f3230d4875ac34fc9d039856b033c6b1430d5d3cc864f87cf89aR1461-R1468 changes the logic of quoting cookies without giving any explanation why.

Previously, 
```
_legal_cookie_chars = frozenset(
    c.encode("ascii")
    for c in f"{string.ascii_letters}{string.digits}/=!#$%&'*+-.^_`|~:"
)
```
included `:` and `_`, now it doesn't:
```
_token_re = re.compile(r"[\w!#$%&'*+\-.^`|~]*", re.A)
```
which causes quoting whole cookie value.

https://www.rfc-editor.org/rfc/rfc6265 doesn't mention colon nor underscore at all in the context of cookie value:
```
 cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
 cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
                       ; US-ASCII characters excluding CTLs,
                       ; whitespace DQUOTE, comma, semicolon,
                       ; and backslash
```

Werkzeug version: 2.3.0


Discussion:
Looks like I was still thinking about `token` chars instead of the extra chars that cookies allow unescaped.
Not sure why you're seeing `_` get quoted, it's part of `\w` in regex.

Resolving patch:
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -1327,7 +1327,7 @@ def parse_cookie(
     )
 
 
-_token_re = re.compile(r"[\w!#$%&'*+\-.^`|~]*", re.A)
+_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)
 _cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A)
 _cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"}
 _cookie_slash_map.update(
@@ -1454,9 +1454,9 @@ def dump_cookie(
         if samesite not in {"Strict", "Lax", "None"}:
             raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
 
-    # This doesn't match RFC 6265. Use quoted-string for non-token values as with header
-    # parameters. Slash-escape controls, comma, and semicolon with three octal digits.
-    if not _token_re.fullmatch(value):
+    # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
+    # three octal digits, which matches http.cookies, although the RFC suggests base64.
+    if not _cookie_no_quote_re.fullmatch(value):
         # Work with bytes here, since a UTF-8 character could be multiple bytes.
         value = _cookie_slash_re.sub(
             lambda m: _cookie_slash_map[m.group()], value.encode(charset)


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -432,8 +432,8 @@ def test_dump_cookie(self):
             "Max-Age=360",
             '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/"
+        assert http.dump_cookie("key", "xxx=") == "key=xxx="
 
     def test_bad_cookies(self):
         cookies = http.parse_cookie(
@@ -456,7 +456,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"
         assert http.parse_cookie(val)["foo"] == "?foo"
         assert http.parse_cookie(r'foo="foo\054bar"')["foo"] == "foo,bar"