Repository navigation
Conversation
- fastapi: run before_request hooks on every route, when mounted and on the WebSocket handshake, and send a hook's returned response. - Background callbacks: key results per run when cache_by is unset and per callback always, so equal inputs never share another request's result. - dcc.Location(refresh=True).href and html.Area.href go through clean_url. - Pages path_template matching: no IndexError or regex backtracking on pathnames containing a newline. - MCP: build the callback adapter collection per request so values computed for one user are not served to another.
|
Dash performance benchmarks✅ all within thresholds
growth = late-third / early-third per-op time; ~1 is flat, a large value means the per-op cost scales with accumulated state. machine scale vs baseline: 0.63x - divided out of the baseline ratios so they compare like for like (the absolute warn/fail ceilings are left un-scaled); calibrated on |
| return result | ||
| status, headers = 200, None | ||
| if isinstance(result, tuple): | ||
| result, status, *rest = result |
There was a problem hiding this comment.
This unpacks every tuple as (body, status[, headers]) but (body, headers) is also valid.
This example breaks:
from dash import Dash, html
app = Dash(__name__, backend="fastapi") # also try "flask" or "quart"
app.layout = html.Div("hi")
def deny():
return ("denied", {"X-Test": "1"})
app.backend.before_request(deny)| except Exception: # pylint: disable=broad-exception-caught | ||
| denied = True |
There was a problem hiding this comment.
Here we close the handshake without logging. A bug in a hook would block every Websocket and leave nothing to debug. I think we should log the exception before denying.
Related: with a hook that denies everyone, FastAPI now rejects the websocket handshake but Quart still connects. Do you care about the inconsistency there?
| # Without caching the job runs in the requester's context, so two | ||
| # requests with equal inputs must never share a result. | ||
| hash_dict["nonce"] = secrets.token_hex(16) |
There was a problem hiding this comment.
With cache_by unset, the output of a background callback can now stay in the cache forever.
Example: a user clicks a button that starts a slow background callback, then refreshes the page before it finishes. The callback still finishes and saves its output, but the refreshed page never asks for it.
Before this change, the next identical click reused the same cache entry and replaced the old output. Now every click gets its own entry, so each refresh leaves one more saved output behind.
We should set an expiry on these outputs, or find some mechanism to safely remove stale entries.
| app.mcp_decorated_functions = dict(MCP_DECORATED_FUNCTIONS) | ||
| # A fresh collection per message: its adapters cache values computed from | ||
| # this caller's request, which must never be served to another user. | ||
| # pylint: disable-next=protected-access | ||
| token = app._mcp_callback_map.set(CallbackAdapterCollection(app)) | ||
| try: | ||
| return _dispatch_mcp_message(method, params, request_id) | ||
| finally: | ||
| app._mcp_callback_map.reset(token) # pylint: disable=protected-access |
There was a problem hiding this comment.
Creating a fresh mcp_callback_map per request will cause significant performance problems in larger apps.
Each request throws away everything the adapters compute lazily. MCP clients in their normal usage will have to walk the layout multiple times and re-run every callback to compute their initial values. (example: a tools/list call will recompute everything every time)
I would suggest keeping the cached mcp_callback_map and instead allow app authors to work around the narrow security issue by supplying their own cache keys. Basically, following the same pattern established by background callbacks’ cache_by kwarg (with a default value that preserves the caching we have today).
| button: createXSSProtection('formAction'), | ||
| area: createXSSProtection('href') |
There was a problem hiding this comment.
Just calling out that html.Base.href remains unsanitized here. Not sure if that's intentional.



Fixes five issues from a responsible-disclosure report against 4.5.0rc1.
FastAPI
before_requesthooks (dash/backends/_fastapi.py)Hooks only ran for
_dash-*routes, the returned response was dropped, and nothing ran when the app was mounted under a parent FastAPI app. Hooks now run for every route of the Dash app (assets, hook routes,api_endpoint, pages), use the path relative toroot_path, and gate the WebSocket handshake. A non-None result is sent as the response (Response, body, or(body, status[, headers])), andHTTPExceptionkeeps its status. User routes still skip the JSON body read and keep their own error handling.Background callback result keys (
background_callback/managers/__init__.py)The key was
sha256(args, fn source, triggered). The signedendIdhandle stops forged keys, but a second client sending the same inputs got a valid handle for the same raw key and could read another user's result. Withoutcache_byeach run now gets a random nonce, and the callback key is always part of the hash, so callbacks from one factory function no longer collide.URL sanitizer gaps (
Location.react.js, htmlgenerate-components.js)dcc.Location(refresh=True).hrefandhtml.Area.hrefnow go throughclean_url, raising the same "Dangerous link detected" error asdcc.Link.Pages
path_templatematching (dash/_pages.py)A newline in the pathname passed the
fnmatchgate but not the unanchored(.*)regex, giving anIndexErrorand polynomial backtracking. Matching now uses a precompiledDOTALLfullmatchafter the gate. Variables can still contain/as before.MCP request isolation (
dash/dash.py,dash/mcp/_server.py)app.mcp_callback_mapcached callback initial outputs and layout values from the first caller for the whole process. It is now a per-appContextVar, set to a fresh collection for each MCP message.Behavior changes
build_cache_keytakes a newcallback_keyargument.cache_byunset, identical triggers no longer reuse a result.Tests
Each new test fails on
devand passes here:tests/backend_tests/test_fastapi_before_request.pytests/integration/security/test_background_isolation.pytests/integration/security/test_xss.py(xss004,xss005, checked against bundles built from unpatched sources)tests/integration/multi_page/test_pages_path_template.pytests/integration/mcp/test_mcp_request_isolation.pytests/unit,tests/backend_tests,tests/integration/{mcp,multi_page,security}andtests/background_callbackpass locally.