Merge branch 'stable'

This commit is contained in:
David Lord 2025-05-13 08:10:30 -07:00
commit e7e5380776
No known key found for this signature in database
GPG key ID: 43368A7AA8CC5926
4 changed files with 24 additions and 11 deletions

View file

@ -9,11 +9,13 @@ Unreleased
Version 3.1.1
-------------
Unreleased
Released 2025-05-13
- Fix type hint for `cli_runner.invoke`. :issue:`5645`
- 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`
- ``flask --help`` loads the app and plugins first to make sure all commands
are shown. :issue:5673`
are shown. :issue:`5673`
- Mark sans-io base class as being able to handle views that return
``AsyncIterable``. This is not accurate for Flask, but makes typing easier
for Quart. :pr:`5659`

View file

@ -127,13 +127,16 @@ The following configuration values are used internally by Flask:
.. py:data:: SECRET_KEY_FALLBACKS
A list of old secret keys that can still be used for unsigning, most recent
first. This allows a project to implement key rotation without invalidating
active sessions or other recently-signed secrets.
A list of old secret keys that can still be used for unsigning. This allows
a project to implement key rotation without invalidating active sessions or
other recently-signed secrets.
Keys should be removed after an appropriate period of time, as checking each
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
:data:`SECRET_KEY` may not support this yet.

View file

@ -318,11 +318,12 @@ class SecureCookieSessionInterface(SessionInterface):
if not app.secret_key:
return None
keys: list[str | bytes] = [app.secret_key]
keys: list[str | bytes] = []
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
keys.extend(fallbacks)
keys.append(app.secret_key) # itsdangerous expects current key at top
return URLSafeTimedSerializer(
keys, # type: ignore[arg-type]
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]:
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()
assert client.get().json == {"a": 1}
# 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 == {}
# Add initial secret key as fallback, session can be loaded
app.config["SECRET_KEY_FALLBACKS"] = ["test key"]
# Rotate the valid keys, session can be loaded
app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = (
"+1 key",
["0 key", "-1 key"],
)
assert client.get().json == {"a": 1}