no cookie domain by default

This commit is contained in:
David Lord 2023-04-12 12:38:22 -07:00
parent fa0ceb62f2
commit c24f8c8199
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
5 changed files with 34 additions and 125 deletions

View File

@ -37,6 +37,10 @@ Unreleased
binary file instead. :issue:`4989`
- If a blueprint is created with an empty name it raises a ``ValueError``.
:issue:`5010`
- ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not
to set the domain, which modern browsers interpret as an exact match rather than
a subdomain match. Warnings about ``localhost`` and IP addresses are also removed.
:issue:`5051`
Version 2.2.4

View File

@ -134,12 +134,17 @@ The following configuration values are used internally by Flask:
.. py:data:: SESSION_COOKIE_DOMAIN
The domain match rule that the session cookie will be valid for. If not
set, the cookie will be valid for all subdomains of :data:`SERVER_NAME`.
If ``False``, the cookie's domain will not be set.
The value of the ``Domain`` parameter on the session cookie. If not set, browsers
will only send the cookie to the exact domain it was set from. Otherwise, they
will send it to any subdomain of the given value as well.
Not setting this value is more restricted and secure than setting it.
Default: ``None``
.. versionchanged:: 2.3
Not set by default, does not fall back to ``SERVER_NAME``.
.. py:data:: SESSION_COOKIE_PATH
The path that the session cookie will be valid for. If not set, the cookie
@ -219,19 +224,14 @@ The following configuration values are used internally by Flask:
Inform the application what host and port it is bound to. Required
for subdomain route matching support.
If set, will be used for the session cookie domain if
:data:`SESSION_COOKIE_DOMAIN` is not set. Modern web browsers will
not allow setting cookies for domains without a dot. To use a domain
locally, add any names that should route to the app to your
``hosts`` file. ::
127.0.0.1 localhost.dev
If set, ``url_for`` can generate external URLs with only an application
context instead of a request context.
Default: ``None``
.. versionchanged:: 2.3
Does not affect ``SESSION_COOKIE_DOMAIN``.
.. py:data:: APPLICATION_ROOT
Inform the application what path it is mounted under by the application /

View File

@ -3,6 +3,7 @@ import pkgutil
import socket
import sys
import typing as t
import warnings
from datetime import datetime
from functools import lru_cache
from functools import update_wrapper
@ -662,7 +663,16 @@ def is_ip(value: str) -> bool:
:return: True if string is an IP address
:rtype: bool
.. deprecated:: 2.3
Will be removed in Flask 2.4.
"""
warnings.warn(
"The 'is_ip' function is deprecated and will be removed in Flask 2.4.",
DeprecationWarning,
stacklevel=2,
)
for family in (socket.AF_INET, socket.AF_INET6):
try:
socket.inet_pton(family, value)

View File

@ -1,6 +1,5 @@
import hashlib
import typing as t
import warnings
from collections.abc import MutableMapping
from datetime import datetime
from datetime import timezone
@ -9,7 +8,6 @@ from itsdangerous import BadSignature
from itsdangerous import URLSafeTimedSerializer
from werkzeug.datastructures import CallbackDict
from .helpers import is_ip
from .json.tag import TaggedJSONSerializer
if t.TYPE_CHECKING: # pragma: no cover
@ -181,62 +179,17 @@ class SessionInterface:
return app.config["SESSION_COOKIE_NAME"]
def get_cookie_domain(self, app: "Flask") -> t.Optional[str]:
"""Returns the domain that should be set for the session cookie.
"""The value of the ``Domain`` parameter on the session cookie. If not set,
browsers will only send the cookie to the exact domain it was set from.
Otherwise, they will send it to any subdomain of the given value as well.
Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
falls back to detecting the domain based on ``SERVER_NAME``.
Uses the :data:`SESSION_COOKIE_DOMAIN` config.
Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
updated to avoid re-running the logic.
.. versionchanged:: 2.3
Not set by default, does not fall back to ``SERVER_NAME``.
"""
rv = app.config["SESSION_COOKIE_DOMAIN"]
# set explicitly, or cached from SERVER_NAME detection
# if False, return None
if rv is not None:
return rv if rv else None
rv = app.config["SERVER_NAME"]
# server name not set, cache False to return none next time
if not rv:
app.config["SESSION_COOKIE_DOMAIN"] = False
return None
# chop off the port which is usually not supported by browsers
# remove any leading '.' since we'll add that later
rv = rv.rsplit(":", 1)[0].lstrip(".")
if "." not in rv:
# Chrome doesn't allow names without a '.'. This should only
# come up with localhost. Hack around this by not setting
# the name, and show a warning.
warnings.warn(
f"{rv!r} is not a valid cookie domain, it must contain"
" a '.'. Add an entry to your hosts file, for example"
f" '{rv}.localdomain', and use that instead."
)
app.config["SESSION_COOKIE_DOMAIN"] = False
return None
ip = is_ip(rv)
if ip:
warnings.warn(
"The session cookie domain is an IP address. This may not work"
" as intended in some browsers. Add an entry to your hosts"
' file, for example "localhost.localdomain", and use that'
" instead."
)
# if this is not an ip and app is mounted at the root, allow subdomain
# matching by adding a '.' prefix
if self.get_cookie_path(app) == "/" and not ip:
rv = f".{rv}"
app.config["SESSION_COOKIE_DOMAIN"] = rv
return rv
return rv if rv else None
def get_cookie_path(self, app: "Flask") -> str:
"""Returns the path for which the cookie should be valid. The

View File

@ -251,36 +251,8 @@ def test_session(app, client):
assert client.get("/get").data == b"42"
def test_session_using_server_name(app, client):
app.config.update(SERVER_NAME="example.com")
@app.route("/")
def index():
flask.session["testing"] = 42
return "Hello World"
rv = client.get("/", "http://example.com/")
cookie = rv.headers["set-cookie"].lower()
# or condition for Werkzeug < 2.3
assert "domain=example.com" in cookie or "domain=.example.com" in cookie
def test_session_using_server_name_and_port(app, client):
app.config.update(SERVER_NAME="example.com:8080")
@app.route("/")
def index():
flask.session["testing"] = 42
return "Hello World"
rv = client.get("/", "http://example.com:8080/")
cookie = rv.headers["set-cookie"].lower()
# or condition for Werkzeug < 2.3
assert "domain=example.com" in cookie or "domain=.example.com" in cookie
def test_session_using_server_name_port_and_path(app, client):
app.config.update(SERVER_NAME="example.com:8080", APPLICATION_ROOT="/foo")
def test_session_path(app, client):
app.config.update(APPLICATION_ROOT="/foo")
@app.route("/")
def index():
@ -288,9 +260,7 @@ def test_session_using_server_name_port_and_path(app, client):
return "Hello World"
rv = client.get("/", "http://example.com:8080/foo")
assert "domain=example.com" in rv.headers["set-cookie"].lower()
assert "path=/foo" in rv.headers["set-cookie"].lower()
assert "httponly" in rv.headers["set-cookie"].lower()
def test_session_using_application_root(app, client):
@ -382,34 +352,6 @@ def test_session_using_samesite_attribute(app, client):
assert "samesite=lax" in cookie
def test_session_localhost_warning(recwarn, app, client):
app.config.update(SERVER_NAME="localhost:5000")
@app.route("/")
def index():
flask.session["testing"] = 42
return "testing"
rv = client.get("/", "http://localhost:5000/")
assert "domain" not in rv.headers["set-cookie"].lower()
w = recwarn.pop(UserWarning)
assert "'localhost' is not a valid cookie domain" in str(w.message)
def test_session_ip_warning(recwarn, app, client):
app.config.update(SERVER_NAME="127.0.0.1:5000")
@app.route("/")
def index():
flask.session["testing"] = 42
return "testing"
rv = client.get("/", "http://127.0.0.1:5000/")
assert "domain=127.0.0.1" in rv.headers["set-cookie"].lower()
w = recwarn.pop(UserWarning)
assert "cookie domain is an IP" in str(w.message)
def test_missing_session(app):
app.secret_key = None