Merge commit from fork

Sessions: fix signing key selection when key rotation is enabled
This commit is contained in:
David Lord 2025-05-13 07:46:54 -07:00 committed by GitHub
commit 73d6504063
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 21 additions and 8 deletions

View File

@ -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`

View File

@ -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.

View File

@ -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,

View File

@ -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}