### Affected version / file / line
Version: djust v1.1.4 (latest release, published 2026-09-23). File: python/djust/mcp/server.py:354-433 (MCP tool find_handlers_for_template). Confirmed identical on the commit used for triage, a7e4fafd1667b4db0b1465652d4a63cb17117a07.
### Description
```
@mcp.tool()
def find_handlers_for_template(template_path: str) -> str:
"""
Args:
template_path: Either a logical template name (e.g.
'demos/counter.html' — what you'd pass to `render`) OR
an absolute filesystem path. Both are searched.
"""
...
resolved_path: str | None = None
if os.path.isabs(template_path) and os.path.isfile(template_path):
resolved_path = template_path
else:
... # logical-name resolution via Django's template loader
...
with open(resolved_path, "r", encoding="utf-8", errors="replace") as f:
source = f.read()
handler_names_in_template = sorted({m.group(2) for m in dj_event_attr_re.finditer(source)})
```
When template_path is an absolute path to an existing file, it is used directly, with no confinement to the project/template directories — no realpath() + prefix check, no allowlist. open(resolved_path, ...) then reads the file.
This is inconsistent with the sibling tool in the same file (loaddata, ~line 290-303), which explicitly rejects paths outside the project directory for its own path-like parameter:
```
candidate = os.path.realpath(...)
if base_dir and not candidate.startswith(os.path.realpath(str(base_dir))):
return json.dumps({"error": f"fixture path '{p}' resolves outside the project directory", ...})
```
find_handlers_for_template has no equivalent check for template_path before the open() call.
### Impact
An MCP client with access to this tool (available whenever the Django project is running the MCP server via python manage.py djust_mcp — no separate opt-in flag gates this specific tool beyond that) can read the contents of any file on disk that the process has read permission to and that exists, by passing an absolute path — e.g. /etc/passwd, application .env files, SSH keys, or any other sensitive file reachable by the server process's user.
Mitigating factor (reduces this from High to Medium): the tool does not return the raw file content. It runs one fixed regex (dj-(?:click|submit|change|input|keydown|keyup)\s*=\s*['"]([^'"]+)['"]) against the content and returns only the matched handler-name strings, plus confirmation that the file exists and is readable (an existence/readability oracle on its own, independent of the regex match). If the targeted file happens to contain text matching that HTML-attribute-like pattern (plausible for config files, logs, or any text file with key="value"-shaped content), those substrings are returned in full — so the read is not purely blind, but it is not a general-purpose arbitrary-file-read primitive either.
Proof of Concept / Reproduction
Traced via direct source read; no live Django project was exercised for this report (would require a full djust project scaffold, which was out of scope for this pass — see limitation below).
1. MCP client calls: find_handlers_for_template(template_path="/etc/passwd")
2. server.py ~line 393:
```
if os.path.isabs(template_path) and os.path.isfile(template_path):
resolved_path = template_path
```
resolved_path == "/etc/passwd", unchanged, unconfined
3. server.py ~line 430:
```
with open(resolved_path, "r", encoding="utf-8", errors="replace") as f:
source = f.read()
```
/etc/passwd is now fully read into `source`
4. The regex dj_event_attr_re.finditer(source) is applied to the real
/etc/passwd content. /etc/passwd will not match this specific pattern, so
the response would report an empty handler list plus "file exists and was
read" - which already confirms the path-confinement bypass (compare with
passing a nonexistent absolute path, which correctly returns
"Template file not found").
5. On a target where the absolute path points to a file that DOES contain
text shaped like `dj-click="..."` (e.g. a copied HTML fragment, a stray
config value, or a crafted file placed by the attacker via another
vector), the matched substrings are returned verbatim in the JSON
response - full-content disclosure of just those substrings, not the
whole file.
### Suggested Fix
Apply the same confinement loaddata already uses, before the os.path.isfile check:
```
if os.path.isabs(template_path):
candidate = os.path.realpath(template_path)
if base_dir and not candidate.startswith(os.path.realpath(str(base_dir))):
return json.dumps({
"error": f"template path '{template_path}' resolves outside the project directory",
"resolved": candidate,
"project_dir": str(base_dir),
})
if os.path.isfile(candidate):
resolved_path = candidate
```
### Limitations
Not exercised against a live Django/dju
CVE-2026-61599 is a high-severity vulnerability in djust versions prior to 1.0.7 that allows unauthenticated WebSocket clients to cause the server to import and execute arbitrary Python modules. This occurs because the live transport resolves LiveView classes from client-supplied module paths using __import__ before validating the class or performing authentication. The default allowlist for permitted modules is fail-open and uses loose matching, enabling unsafe reflection and code execution. Version 1.0.7 introduces a fail-closed check that restricts imports to already loaded modules or explicitly allowed modules, preventing this unsafe behavior.
CVE-2026-61589 affects djust versions prior to 1.0.7 and involves incorrect tenant resolution due to the use of a less trusted source for the HTTP host in WebSocket request reconstruction. This causes tenant-scoped data managers to either return unscoped data (cross-tenant data disclosure) when STRICT_MODE is false or empty querysets when STRICT_MODE is true. The issue is fixed in version 1.0.7 by properly extracting and validating the Host header from the ASGI scope against ALLOWED_HOSTS.
CVE-2026-61596 is an authorization bypass vulnerability in djust prior to version 1.0.7. The flaw allows authenticated users to view or act on objects they are not authorized for via certain render entry points that did not enforce per-object authorization. This includes the initial HTTP GET render, SPA url_change navigation, and embedded child views using {% live_render %}. The issue is fixed in version 1.0.7 by routing all render entry points through a shared permission enforcement chokepoint. No reliable workaround exists other than upgrading.
djust provides Phoenix LiveView-style reactive server-side rendering for Django with Rust-powered performance. Prior to version 1.0.7, when a Django `Model` instance is assigned to a public view attribute, djust serialized it to the client with no sensitive-field denylist — sending fields such as `password` (the hash), privilege flags (e.g. `is_staff` / `is_superuser`), tokens, and other PII to the browser. Because exposing model objects to templates is a normal djust pattern, this could leak credentials/PII without the developer realizing the full object crossed the wire. This is fixed in djust 1.0.7. Model serialization applies a secure-by-default sensitive-field denylist (password/hash/token/secret-style fields and known privilege flags are withheld) with an identity-subset fallback. As a workaround, keep `Model` instances on `_private` attributes and expose only the specific fields needed, until patched.
CVE-2026-61594 is a critical vulnerability in djust versions prior to 1.0.7 where WebSocket transport bypasses standard Django authorization mechanisms. This allows anonymous or under-privileged clients to mount views and dispatch handlers over WebSocket that should be protected, including admin functions. The issue arises because authorization is checked via a custom method that does not enforce Django's usual access controls on WebSocket connections. The vulnerability is fixed in djust 1.0.7 by enforcing Django AccessMixin checks on all transports and introducing startup checks for unsafe auth patterns.
djust versions prior to 1.0.7 have a vulnerability where state snapshots embedded in client pages are not integrity-checked. This allows a client to modify unsigned state data, such as privilege flags or account information, and send it back to the server to escalate privileges or tamper with business state. The issue is fixed in version 1.0.7 by signing state snapshots and rejecting unsigned or forged snapshots. As a workaround, disabling state snapshots or avoiding storing authorization state in public view attributes mitigates the risk.
CVE-2026-61592 is a session fixation vulnerability in djust versions prior to 1.0.7. The issue arises because SSE sessions were keyed solely by a client-chosen session_id without binding to the authenticated user, allowing an attacker who obtains a valid session_id to impersonate the victim. This vulnerability is fixed in version 1.0.7 by binding SSE sessions to the owning principal and rejecting cross-principal access. A workaround is to disable the SSE transport.
CVE-2026-61597 is a cross-site scripting (XSS) vulnerability in djust versions prior to 1.0.7. The issue arises because certain built-in component template tags render user-supplied URLs into href or action attributes with HTML escaping but do not validate the URL scheme. This allows javascript: URIs to be injected and executed in the victim's browser when clicked. The vulnerability is fixed in version 1.0.7.
djust provides Phoenix LiveView-style reactive server-side rendering for Django with Rust-powered performance. Prior to version 1.0.7, the SSE client→server POST endpoints are `@csrf_exempt` and the SSE GET stream endpoint had no Origin check, so a cross-origin page could drive a victim-cookie-authenticated SSE session: force the victim's browser to GET the stream URL (which creates and mounts a LiveView as the victim) and POST to the message endpoint with `credentials: include` to fire state-changing event handlers as the victim. The URL `session_id` is client-chosen (validated only for UUID *format*), so it is not a CSRF token, and a JSON body sent as `text/plain` is a CORS simple request with no preflight. The issue is fixed in 1.0.7. All three SSE endpoints validate the request `Origin` against `ALLOWED_HOSTS` (mirroring the WebSocket CSWSH defense) and reject cross-origin requests with 403; the POST endpoints additionally require `Content-Type: application/json` (415 otherwise), closing the `text/plain` simple-request bypass. As a workaround, disable the SSE transport, or front it with a proxy that enforces an Origin allowlist.
djust provides Phoenix LiveView-style reactive server-side rendering for Django with Rust-powered performance. Prior to version 1.0.7, `djust.tenants` isolation was enforced only on the HTTP path. The current tenant was stored in `threading.local()` and set exclusively by the HTTP-only `TenantMiddleware`, so on the live (WebSocket/SSE) path `get_current_tenant()` was always `None` during mount and every event handler — and the tenant-aware `QuerySet` manager failed OPEN (returned the unfiltered queryset, ignoring `STRICT_MODE`), disclosing every tenant's rows to whoever held the socket. `threading.local` was additionally shared across connections on the `sync_to_async` executor thread. This issue is fixed in djust 1.0.7. Tenant storage moved to a `contextvars.ContextVar` (per async task); the resolved tenant is bound around WS/SSE mount and every dispatch; both managers scope the base queryset once and fail CLOSED (`.none()` under the default `STRICT_MODE`); and system check S006 warns when `STRICT_MODE=False`. No known workarounds are available on the live path.
djust provides Phoenix LiveView-style reactive server-side rendering for Django with Rust-powered performance. Prior to version 1.0.7, `djust.mixins.model_binding.ModelBindingMixin` provides a default `update_model` event handler and is part of the LiveView base MRO, so every LiveView exposes it. It `setattr`s a view attribute whose name is client-supplied (`field`), gated only by: reject `_`-prefixed names; reject a 14-entry denylist of framework internals (`FORBIDDEN_MODEL_FIELDS`); optional `allowed_model_fields` which defaults to None = allow all; and `hasattr` existence. As a result, a client can set any public, existing view attribute — not just the fields actually bound with `dj-model=` in the rendered template. The denylist covers framework plumbing but nothing about developer business/authz state, and the allowlist is opt-in (off by default). A developer who binds one `dj-model="search"` input and also keeps `self.account_id` / `self.is_admin` / `self.total_price` as view state does not realize a client can set ALL of them via `{type:event, event:"update_model", params:{field, value}}` over the WebSocket. Type coercion matches the target attribute's type (so `"true"` -> bool True), aiding the attacker. This issue is fixed in djust 1.0.7. As a workaround, set `allowed_model_fields` explicitly on every view using dj-model (or subclassing LiveView) to the minimal list of bindable fields; do not keep authorization/ownership state in public view attributes that share the view with dj-model bindings.
djust provides Phoenix LiveView-style reactive server-side rendering for Django with Rust-powered performance. Prior to version 1.0.7, djust's observability endpoints expose live view/session state and a remote method-invocation surface (`eval_handler`). The localhost restriction was an opt-in middleware that the documented setup omits; the views themselves enforced only `DEBUG`. In the misconfigured-but-documented scenario (DEBUG on, middleware not installed) a non-localhost client could read live application state and invoke handlers remotely. This issue is fixed in djust 1.0.7. The localhost restriction is enforced in-view on every observability endpoint (no longer dependent on a separately-installed middleware), and `eval_handler` is restricted; gated requests receive a non-disclosing response. As a workaround, ensure `DEBUG=False` in production, and do not expose the observability endpoints to untrusted networks.
CVE-2026-55571 is an improper authorization vulnerability in djust prior to version 1.0.4. The issue occurs because the LiveViewConsumer.handle_mount method sends a navigation frame on authorization failure but does not close the WebSocket or clear the view instance. This allows raw WebSocket clients to bypass authentication checks and invoke event handler methods without a valid session, potentially leading to unauthorized sensitive reads or mutations. The vulnerability is fixed in version 1.0.4.