Bug description
#35237 (abc2d46fed, in 6.1.0) removed the only consumer of Flask's flash queue but left the
producers in place. get_flashed_messages() is what pops _flashes out of the session; it used to
run on every SPA render:
--- a/superset/views/base.py
+++ b/superset/views/base.py
def common_bootstrap_payload() -> dict[str, Any]:
- return {
- **cached_common_bootstrap_data(utils.get_user_id(), get_locale()),
- "flash_messages": get_flashed_messages(with_categories=True),
- }
+ return cached_common_bootstrap_data(utils.get_user_id(), get_locale())
It now appears nowhere in superset/, superset/templates/ or superset-frontend/src, and the same
commit deleted the frontend FlashProvider that displayed the messages. Flask-AppBuilder's auth
views still flash on every failed login, as do
superset/security/session_invalidation.py:132 and superset/security/password_change.py:176.
So _flashes grows for the life of the session and nothing ever renders it. Two consequences:
- The default client-side session cookie grows without bound (
SESSION_SERVER_SIDE = False),
about 57 bytes per failed login attempt, carried on every request.
- Login failures are invisible.
superset-frontend/src/pages/Login/index.tsx papers over the
DB-credentials path by fabricating a message from sessionStorage, behind a TODO admitting the
gap. There is no equivalent for OAuth, session invalidation, or forced password change.
Minimal reproduction
Default config, AUTH_TYPE = AUTH_DB, SESSION_SERVER_SIDE = False:
-
Go to /login/ and submit wrong credentials three times.
-
Decode the session cookie — Flask signs but does not encrypt it, so no secret is needed:
import base64, json, zlib
cookie = "<value of the session cookie>"
raw = cookie.split(".")[1] if cookie.startswith(".") else cookie.split(".")[0]
data = base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4))
print(json.loads(zlib.decompress(data) if cookie.startswith(".") else data)["_flashes"])
Observed: three copies of ["warning", "Invalid login. Please try again."], retained for the life
of the session, none of them ever shown.
Expected: the queue is drained when the login page renders, as it was before 6.1.0.
Why it matters beyond cookie size
Under AUTH_TYPE = AUTH_OAUTH the same accumulation stacks with Authlib's stale _state_* handshake
entries (an Authlib/FAB issue, not this one). On the fourth consecutive refusal by the IdP, the 302
carrying Set-Cookie + Location + CSP exceeded ingress-nginx's default 4k proxy_buffer_size and
the user got HTTP 502 instead of a login page — having never been told why any attempt failed.
Past roughly six entries the cookie also crosses the browser's 4096-byte cap and the whole session,
csrf_token included, is silently dropped.
Suggested fix
Drain and display at the login view, which addresses both consequences at once.
superset/views/auth.py:
from flask import g, get_flashed_messages, redirect
@expose("/")
@no_cache
def login(self, provider: Optional[str] = None) -> WerkzeugResponse:
if g.user is not None and g.user.is_authenticated:
return redirect(self.appbuilder.get_url_for_index)
return super().render_app_template(
{"auth_messages": get_flashed_messages(with_categories=True)}
)
render_app_template's first argument is extra_bootstrap_data, which get_spa_payload merges per
request. It must go there and not back into common_bootstrap_payload(), whose
cached_common_bootstrap_data is @cache_manager.cache.memoize(timeout=60) — per-user messages
there would bleed between users, which is presumably why #35237 deleted rather than relocated them.
Then render bootstrapData.auth_messages in superset-frontend/src/pages/Login/index.tsx (replacing
the sessionStorage hack) and add auth_messages?: [string, string][] to BootstrapData in
superset-frontend/src/types/bootstrapTypes.ts.
The alternative — removing the remaining flash() calls instead — bounds the cookie but leaves users
with no feedback on a failed login.
Screenshots/recordings
No response
Superset version
master / latest-dev
Python version
3.11
Node version
18 or greater
Browser
Chrome
Additional context
k8s ingress controller logs:
[error] 1103#1103: *10079930 upstream sent too big header while reading response header from upstream
- No feature flags involved.
SESSION_SERVER_SIDE = False is what puts the growth in the cookie
rather than a server-side store; enabling server-side sessions hides the symptom but the queue
still grows in the store.
- No Python stacktrace: nothing raises. FAB logs the auth error and redirects; the 502 in the OAuth
case comes from ingress-nginx.
- Separate docs nit, if useful:
docs/admin_docs/security/security.mdx says the session cookie "is
encrypted with the application SECRET_KEY and cannot be read by the client". It is signed, not
encrypted — the snippet above needs no secret.
related?
#30302
Checklist
Bug description
#35237(abc2d46fed, in 6.1.0) removed the only consumer of Flask's flash queue but left theproducers in place.
get_flashed_messages()is what pops_flashesout of the session; it used torun on every SPA render:
It now appears nowhere in
superset/,superset/templates/orsuperset-frontend/src, and the samecommit deleted the frontend
FlashProviderthat displayed the messages. Flask-AppBuilder's authviews still flash on every failed login, as do
superset/security/session_invalidation.py:132andsuperset/security/password_change.py:176.So
_flashesgrows for the life of the session and nothing ever renders it. Two consequences:SESSION_SERVER_SIDE = False),about 57 bytes per failed login attempt, carried on every request.
superset-frontend/src/pages/Login/index.tsxpapers over theDB-credentials path by fabricating a message from
sessionStorage, behind a TODO admitting thegap. There is no equivalent for OAuth, session invalidation, or forced password change.
Minimal reproduction
Default config,
AUTH_TYPE = AUTH_DB,SESSION_SERVER_SIDE = False:Go to
/login/and submit wrong credentials three times.Decode the
sessioncookie — Flask signs but does not encrypt it, so no secret is needed:Observed: three copies of
["warning", "Invalid login. Please try again."], retained for the lifeof the session, none of them ever shown.
Expected: the queue is drained when the login page renders, as it was before 6.1.0.
Why it matters beyond cookie size
Under
AUTH_TYPE = AUTH_OAUTHthe same accumulation stacks with Authlib's stale_state_*handshakeentries (an Authlib/FAB issue, not this one). On the fourth consecutive refusal by the IdP, the 302
carrying
Set-Cookie+Location+ CSP exceeded ingress-nginx's default 4kproxy_buffer_sizeandthe user got HTTP 502 instead of a login page — having never been told why any attempt failed.
Past roughly six entries the cookie also crosses the browser's 4096-byte cap and the whole session,
csrf_tokenincluded, is silently dropped.Suggested fix
Drain and display at the login view, which addresses both consequences at once.
superset/views/auth.py:render_app_template's first argument isextra_bootstrap_data, whichget_spa_payloadmerges perrequest. It must go there and not back into
common_bootstrap_payload(), whosecached_common_bootstrap_datais@cache_manager.cache.memoize(timeout=60)— per-user messagesthere would bleed between users, which is presumably why #35237 deleted rather than relocated them.
Then render
bootstrapData.auth_messagesinsuperset-frontend/src/pages/Login/index.tsx(replacingthe
sessionStoragehack) and addauth_messages?: [string, string][]toBootstrapDatainsuperset-frontend/src/types/bootstrapTypes.ts.The alternative — removing the remaining
flash()calls instead — bounds the cookie but leaves userswith no feedback on a failed login.
Screenshots/recordings
No response
Superset version
master / latest-dev
Python version
3.11
Node version
18 or greater
Browser
Chrome
Additional context
k8s ingress controller logs:
[error] 1103#1103: *10079930 upstream sent too big header while reading response header from upstream
SESSION_SERVER_SIDE = Falseis what puts the growth in the cookierather than a server-side store; enabling server-side sessions hides the symptom but the queue
still grows in the store.
case comes from ingress-nginx.
docs/admin_docs/security/security.mdxsays the session cookie "isencrypted with the application
SECRET_KEYand cannot be read by the client". It is signed, notencrypted — the snippet above needs no secret.
related?
#30302
Checklist