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 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 - ``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 - Mark sans-io base class as being able to handle views that return
``AsyncIterable``. This is not accurate for Flask, but makes typing easier ``AsyncIterable``. This is not accurate for Flask, but makes typing easier
for Quart. :pr:`5659` for Quart. :pr:`5659`

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}