secret key rotation: fix key list ordering

The `itsdangerous` serializer interface[1] expects keys to be
provided with the oldest key at index zero and the active signing key
at the end of the list.

We document[2] that `SECRET_KEY_FALLBACKS` should be configured with
the most recent first (at index zero), so to achieve the expected
behaviour, those should be inserted in reverse-order at the head of
the list.

[1] - https://itsdangerous.palletsprojects.com/en/stable/serializer/#itsdangerous.serializer.Serializer

[2] - https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY_FALLBACKS
This commit is contained in:
James Addison 2025-03-10 16:34:12 +00:00 committed by David Lord
parent 941efd4a36
commit fb54159861
No known key found for this signature in database
GPG key ID: 43368A7AA8CC5926
3 changed files with 15 additions and 5 deletions

View file

@ -3,6 +3,8 @@ Version 3.1.1
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`
- ``flask --help`` loads the app and plugins first to make sure all commands
are shown. :issue:5673`

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}