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).
with open(resolved_path, "r", encoding="utf-8", errors="replace") as f:
source = f.read()
/etc/passwd is now fully read into source
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").
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/djust project — this is a source-level trace of the exact executed branch (os.path.isabs + os.path.isfile + open()), not an end-to-end runtime capture.
Did not verify whether any MCP-host-level confirmation/approval step (some MCP clients prompt the user before a tool call with a suspicious-looking path argument) would intercept this in practice - the vulnerability is in the server's own missing server-side guard regardless of client-side UX.
Disclosure Timeline
2026-09-24: found during audit, confirmed via source trace to the open() sink; classified Medium due to the response-content limitation (matched substrings only, not raw file content).
Please check the log file, and apply CVE once the fix is made.
The djust MCP server (manage.py djust_mcp, python -m djust.mcp) only serves stdio to the MCP client that launched it, as the same OS user, on the developer's machine; there is no network listener. The realistic caller is a local AI agent acting on untrusted input. The tool returns only dj-*-attribute-shaped substrings and an existence answer, not file contents. We therefore rate this Low (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N, 3.3).
Affected: every release with the tool, 0.9.0 – 1.0.9, 1.1.0 – 1.1.6, 1.2.0 – 1.2.3, 1.3.0rc4 – 1.3.0rc7.
Patches
Fixed in 1.0.10, 1.1.7, 1.2.4 and 1.3.0rc8. find_handlers_for_template reads an absolute path only when it resolves (symlinks and .. followed) inside a directory the template loaders serve or inside BASE_DIR, and answers any other absolute path like a missing template. The seed_fixtures guard compared paths with a string prefix (so /app-data passed for a BASE_DIR of /app) and skipped the check when BASE_DIR was unset; it now compares whole path components and refuses without BASE_DIR. Both use one containment helper.
Workarounds
Run the MCP server only in projects whose agent you trust with file access, or pass logical template names only.