mirror of https://github.com/pallets/flask.git
Merge commit from fork
Sessions: fix signing key selection when key rotation is enabled
This commit is contained in:
commit
73d6504063
|
@ -3,6 +3,8 @@ Version 3.1.1
|
||||||
|
|
||||||
Unreleased
|
Unreleased
|
||||||
|
|
||||||
|
- Fix signing key selection order when key rotation is enabled via
|
||||||
|
``SECRET_KEY_FALLBACKS``. :ghsa:`4grg-w6v8-c28g`
|
||||||
- Fix type hint for `cli_runner.invoke`. :issue:`5645`
|
- Fix type hint for `cli_runner.invoke`. :issue:`5645`
|
||||||
- ``flask --help`` loads the app and plugins first to make sure all commands
|
- ``flask --help`` loads the app and plugins first to make sure all commands
|
||||||
are shown. :issue:5673`
|
are shown. :issue:5673`
|
||||||
|
|
|
@ -127,13 +127,16 @@ The following configuration values are used internally by Flask:
|
||||||
|
|
||||||
.. py:data:: SECRET_KEY_FALLBACKS
|
.. py:data:: SECRET_KEY_FALLBACKS
|
||||||
|
|
||||||
A list of old secret keys that can still be used for unsigning, most recent
|
A list of old secret keys that can still be used for unsigning. This allows
|
||||||
first. This allows a project to implement key rotation without invalidating
|
a project to implement key rotation without invalidating active sessions or
|
||||||
active sessions or other recently-signed secrets.
|
other recently-signed secrets.
|
||||||
|
|
||||||
Keys should be removed after an appropriate period of time, as checking each
|
Keys should be removed after an appropriate period of time, as checking each
|
||||||
additional key adds some overhead.
|
additional key adds some overhead.
|
||||||
|
|
||||||
|
Order should not matter, but the default implementation will test the last
|
||||||
|
key in the list first, so it might make sense to order oldest to newest.
|
||||||
|
|
||||||
Flask's built-in secure cookie session supports this. Extensions that use
|
Flask's built-in secure cookie session supports this. Extensions that use
|
||||||
:data:`SECRET_KEY` may not support this yet.
|
:data:`SECRET_KEY` may not support this yet.
|
||||||
|
|
||||||
|
|
|
@ -318,11 +318,12 @@ class SecureCookieSessionInterface(SessionInterface):
|
||||||
if not app.secret_key:
|
if not app.secret_key:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
keys: list[str | bytes] = [app.secret_key]
|
keys: list[str | bytes] = []
|
||||||
|
|
||||||
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
|
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
|
||||||
keys.extend(fallbacks)
|
keys.extend(fallbacks)
|
||||||
|
|
||||||
|
keys.append(app.secret_key) # itsdangerous expects current key at top
|
||||||
return URLSafeTimedSerializer(
|
return URLSafeTimedSerializer(
|
||||||
keys, # type: ignore[arg-type]
|
keys, # type: ignore[arg-type]
|
||||||
salt=self.salt,
|
salt=self.salt,
|
||||||
|
|
|
@ -381,14 +381,21 @@ def test_session_secret_key_fallbacks(app, client) -> None:
|
||||||
def get_session() -> dict[str, t.Any]:
|
def get_session() -> dict[str, t.Any]:
|
||||||
return dict(flask.session)
|
return dict(flask.session)
|
||||||
|
|
||||||
# Set session with initial secret key
|
# Set session with initial secret key, and two valid expiring keys
|
||||||
|
app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = (
|
||||||
|
"0 key",
|
||||||
|
["-1 key", "-2 key"],
|
||||||
|
)
|
||||||
client.post()
|
client.post()
|
||||||
assert client.get().json == {"a": 1}
|
assert client.get().json == {"a": 1}
|
||||||
# Change secret key, session can't be loaded and appears empty
|
# Change secret key, session can't be loaded and appears empty
|
||||||
app.secret_key = "new test key"
|
app.secret_key = "? key"
|
||||||
assert client.get().json == {}
|
assert client.get().json == {}
|
||||||
# Add initial secret key as fallback, session can be loaded
|
# Rotate the valid keys, session can be loaded
|
||||||
app.config["SECRET_KEY_FALLBACKS"] = ["test key"]
|
app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = (
|
||||||
|
"+1 key",
|
||||||
|
["0 key", "-1 key"],
|
||||||
|
)
|
||||||
assert client.get().json == {"a": 1}
|
assert client.get().json == {"a": 1}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue