Repository: falconry/falcon

Issue:
Remove browser warning in unset_cookie
Currently unset cookie returns this warning on firefox when used in http:
`Cookie “<name-here>” will be soon rejected because it has the “sameSite” attribute set to “none” or an invalid value, without the “secure” attribute. To know more about the “sameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite`

This particular cookie was set with `secure=False, http_only=True, same_site="Strict"` but I don't think how how it was previously set matters.

From the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings) there seem to be two options to remove this warning:
- Set`same_site` to `Strict` or `Lax`
- Set `secure` to `True`

Setting `same_site` seems preferable since secure can be set only in https for some cookie prefixes: https: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Cookie_prefixes

Since the cookie is removed after the response, setting `same_site` to `Lax` should probably be ok.


Discussion:
Hi :wave:,

Thanks for using Falcon. The large amount of time and effort needed to
maintain the project and develop new features is not sustainable without
the generous financial support of community members like you.

Please consider helping us secure the future of the Falcon framework with a
one-time or recurring donation.

Thank you for your support!


Resolving patch:
diff --git a/falcon/response.py b/falcon/response.py
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -476,17 +476,53 @@ def set_cookie(self, name, value, expires=None, max_age=None,
 
             self._cookies[name]['samesite'] = same_site.capitalize()
 
-    def unset_cookie(self, name):
-        """Unset a cookie in the response
+    def unset_cookie(self, name, domain=None, path=None):
+        """Unset a cookie in the response.
 
         Clears the contents of the cookie, and instructs the user
         agent to immediately expire its own copy of the cookie.
 
+        Note:
+            Modern browsers place restriction on cookies without the
+            "same-site" cookie attribute set. To that end this attribute
+            is set to ``'Lax'`` by this method.
+
+            (See also: `Same-Site warnings`_)
+
         Warning:
             In order to successfully remove a cookie, both the
             path and the domain must match the values that were
             used when the cookie was created.
-        """
+
+        Args:
+            name (str): Cookie name
+
+        Keyword Args:
+            domain (str): Restricts the cookie to a specific domain and
+                    any subdomains of that domain. By default, the user
+                    agent will return the cookie only to the origin server.
+                    When overriding this default behavior, the specified
+                    domain must include the origin server. Otherwise, the
+                    user agent will reject the cookie.
+
+                    (See also: RFC 6265, Section 4.1.2.3)
+
+            path (str): Scopes the cookie to the given path plus any
+                subdirectories under that path (the "/" character is
+                interpreted as a directory separator). If the cookie
+                does not specify a path, the user agent defaults to the
+                path component of the requested URI.
+
+                Warning:
+                    User agent interfaces do not always isolate
+                    cookies by path, and so this should not be
+                    considered an effective security measure.
+
+                (See also: RFC 6265, Section 4.1.2.4)
+
+        .. _Same-Site warnings:
+            https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings
+        """  # noqa: E501
         if self._cookies is None:
             self._cookies = http_cookies.SimpleCookie()
 
@@ -499,6 +535,16 @@ def unset_cookie(self, name):
         # thus removing it from future request objects.
         self._cookies[name]['expires'] = -1
 
+        # NOTE(CaselIT): Set SameSite to Lax to avoid setting invalid cookies.
+        # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings  # noqa: E501
+        self._cookies[name]['samesite'] = 'Lax'
+
+        if domain:
+            self._cookies[name]['domain'] = domain
+
+        if path:
+            self._cookies[name]['path'] = path
+
     def get_header(self, name, default=None):
         """Retrieve the raw string value for the given header.
 


Test patch:
diff --git a/tests/test_cookies.py b/tests/test_cookies.py
--- a/tests/test_cookies.py
+++ b/tests/test_cookies.py
@@ -77,12 +77,21 @@ def on_delete(self, req, resp):
         resp.set_cookie('baz', 'foo', same_site='')
 
 
+class CookieUnset:
+    def on_get(self, req, resp):
+        resp.unset_cookie('foo')
+        resp.unset_cookie('bar', path='/bar')
+        resp.unset_cookie('baz', domain='www.example.com')
+        resp.unset_cookie('foobar', path='/foo', domain='www.example.com')
+
+
 @pytest.fixture
 def client(asgi):
     app = create_app(asgi)
     app.add_route('/', CookieResource())
     app.add_route('/test-convert', CookieResourceMaxAgeFloatString())
     app.add_route('/same-site', CookieResourceSameSite())
+    app.add_route('/unset-cookie', CookieUnset())
 
     return testing.TestClient(app)
 
@@ -151,6 +160,7 @@ def test_response_complex_case(client):
     cookie = result.cookies['bad']
     assert cookie.value == ''  # An unset cookie has an empty value
     assert cookie.domain is None
+    assert cookie.same_site == 'Lax'
 
     assert cookie.expires < datetime.utcnow()
 
@@ -165,6 +175,24 @@ def test_response_complex_case(client):
     assert cookie.secure
 
 
+def test_unset_cookies(client):
+    result = client.simulate_get('/unset-cookie')
+
+    assert len(result.cookies) == 4
+
+    def test(cookie, path, domain):
+        assert cookie.value == ''  # An unset cookie has an empty value
+        assert cookie.domain == domain
+        assert cookie.path == path
+        assert cookie.same_site == 'Lax'
+        assert cookie.expires < datetime.utcnow()
+
+    test(result.cookies['foo'], path=None, domain=None)
+    test(result.cookies['bar'], path='/bar', domain=None)
+    test(result.cookies['baz'], path=None, domain='www.example.com')
+    test(result.cookies['foobar'], path='/foo', domain='www.example.com')
+
+
 def test_cookie_expires_naive(client):
     result = client.simulate_post('/')
 
@@ -233,7 +261,7 @@ def test_response_unset_cookie(client):
     len(morsels) == 1
 
     bad_cookie = morsels[0]
-    bad_cookie['expires'] == -1
+    assert bad_cookie['expires'] == -1
 
     output = bad_cookie.OutputString()
     assert 'bad=;' in output or 'bad="";' in output