Repository: sanic-org/sanic (python)

Issue:
Cookie totally breaks if the client sets a bare cookie
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

A cookie may be not in the `key=value` format. For example. if the JS code runs `document.cookie = "bad"`, it becomes:

![image](https://github.com/sanic-org/sanic/assets/6646473/c9a1f53c-62bf-4516-9003-49fed08773dc)

I don't know how to call it. I will use the term "bare cookie" in this report. In the following requests with a bare cookie, the Cookie HTTP header becomes: `Cookie: key=value; bad`

![image](https://github.com/sanic-org/sanic/assets/6646473/536bdf38-61ad-4f31-bb78-6c7844433298)

It seems that Sanic cannot parse the header with bare cookies, and will throw all cookies (including the legimit `key=value` pair) away. See the code snippet below.

### Code snippet

```python
from sanic import Sanic
from sanic.response import html, text

app = Sanic("test")
app.config.AUTO_EXTEND = False

@app.get("/")
async def route1(request):
    return html('<script>document.cookie="key=value"; document.cookie="bad"; location.href="/fire";</script>')
    
@app.get("/fire")
async def route2(request):
    return text(f'''
        headers = {request.headers.get("Cookie")}
        key = {request.cookies.get("key", "none")}
    ''')

if __name__ == '__main__':
    app.run(port=4321, debug=True)
```

Then visit `http://127.0.0.1:4321/` in Chrome. The page shows:

```
headers = key=value; bad
key = none
```

### Expected Behavior

The page should show:

```
headers = key=value; bad
key = value
```

### How do you run Sanic?

As a script (`app.run` or `Sanic.serve`)

### Operating System

Windows

### Sanic Version

22.12.0

### Additional context

I am using the latest stable Chrome (117.0.5938.150) to reproduce this.




Pull request:
Accept bare cookies
Browsers allow setting `document.cookie="foo"` which causes them to send `cookie: foo`, rather than the usual for `cookie: bar=foo`. WhatWG specs were altered around 2016 to note how Chrome had been doing this, with the meaning that cookie name is empty and only value is included.

Discussion in:
https://github.com/httpwg/http-extensions/issues/159
https://github.com/httpwg/http-extensions/pull/1018

Fix #2835 


Patch:
diff --git a/sanic/cookies/request.py b/sanic/cookies/request.py
index 456c8872..06eab4f1 100644
--- a/sanic/cookies/request.py
+++ b/sanic/cookies/request.py
@@ -73,12 +73,17 @@ def parse_cookie(raw: str) -> Dict[str, List[str]]:
     cookies: Dict[str, List[str]] = {}
 
     for token in raw.split(";"):
-        name, __, value = token.partition("=")
+        name, sep, value = token.partition("=")
         name = name.strip()
         value = value.strip()
 
-        if not name:
-            continue
+        # Support cookies =value or plain value with no name
+        # https://github.com/httpwg/http-extensions/issues/159
+        if not sep:
+            if not name:
+                # Empty value like ;; or a cookie header with no value
+                continue
+            name, value = "", name
 
         if COOKIE_NAME_RESERVED_CHARS.search(name):  # no cov
             continue
diff --git a/sanic/models/protocol_types.py b/sanic/models/protocol_types.py
index 61924a6c..a402cf1f 100644
--- a/sanic/models/protocol_types.py
+++ b/sanic/models/protocol_types.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import sys
 
 from asyncio import BaseTransport
-from typing import TYPE_CHECKING, Any, AnyStr, Optional
+from typing import TYPE_CHECKING, Any, Optional, Union
 
 
 if TYPE_CHECKING:
@@ -19,10 +19,10 @@ else:
     from typing import Protocol
 
     class HTMLProtocol(Protocol):
-        def __html__(self) -> AnyStr:
+        def __html__(self) -> Union[str, bytes]:
             ...
 
-        def _repr_html_(self) -> AnyStr:
+        def _repr_html_(self) -> Union[str, bytes]:
             ...
 
     class Range(Protocol):


Test patch:
diff --git a/tests/test_cookies.py b/tests/test_cookies.py
index 547cdd42..976ddabd 100644
--- a/tests/test_cookies.py
+++ b/tests/test_cookies.py
@@ -7,12 +7,28 @@ import pytest
 from sanic import Request, Sanic
 from sanic.compat import Header
 from sanic.cookies import Cookie, CookieJar
-from sanic.cookies.request import CookieRequestParameters
+from sanic.cookies.request import CookieRequestParameters, parse_cookie
 from sanic.exceptions import ServerError
 from sanic.response import text
 from sanic.response.convenience import json
 
 
+def test_request_cookies():
+    cdict = parse_cookie("foo=one; foo=two; abc = xyz;;bare;=bare2")
+    assert cdict == {
+        "foo": ["one", "two"],
+        "abc": ["xyz"],
+        "": ["bare", "bare2"],
+    }
+    c = CookieRequestParameters(cdict)
+    assert c.getlist("foo") == ["one", "two"]
+    assert c.getlist("abc") == ["xyz"]
+    assert c.getlist("") == ["bare", "bare2"]
+    assert (
+        c.getlist("bare") == None
+    )  # [] might be sensible but we got None for now
+
+
 # ------------------------------------------------------------ #
 #  GET
 # ------------------------------------------------------------ #
diff --git a/tests/test_graceful_shutdown.py b/tests/test_graceful_shutdown.py
index d125ba3d..7b1ceb62 100644
--- a/tests/test_graceful_shutdown.py
+++ b/tests/test_graceful_shutdown.py
@@ -1,6 +1,8 @@
 import asyncio
 import logging
 
+import pytest
+
 from pytest import LogCaptureFixture
 
 from sanic.response import empty
@@ -9,6 +11,7 @@ from sanic.response import empty
 PORT = 42101
 
 
+@pytest.mark.xfail(reason="This test runs fine locally, but fails on CI")
 def test_no_exceptions_when_cancel_pending_request(
     app, caplog: LogCaptureFixture
 ):