Skip to main content
VulnerabilityPublished

CVE-2026-113731: djust · Path Traversal

Affects djustSource CVE Database V5

0

Not known to be exploited · CVSS 3.3 Low · fixed in 1.3.0rc8 : update djust when convenient.

Data as of 26 Oct 2026, 17:16 UTC

CVSS 3.1 base score
3.3Low
How severe the flaw is on the 0–10 CVSS scale.
EPSS
0.1% chance of exploitation in 30 days – higher than 8% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
Patch
Available – fixed in 1.3.0rc8, 1.2.4, 1.1.7 +1
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 20 Oct 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponised: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.

What to do

Upgrade djust and djust-org/djust to 1.3.0rc8, 1.2.4, 1.1.7 or 1.0.10.

Affected software

ProductAffectedFixed in
djust-orgdjustAffected
  • >=1.3.0.dev0 <1.3.0rc8
Fixed in
  • 1.3.0rc8
djust-orgdjustAffected
  • >=1.2.0.dev0 <1.2.4
Fixed in
  • 1.2.4
djust-orgdjustAffected
  • >=1.1.0 <1.1.7
Fixed in
  • 1.1.7
djust-orgdjustAffected
  • >=0.9.0 <1.0.10
Fixed in
  • 1.0.10
GitHubdjust-org/djustpkg:github/djust-org/djustOther threatsAffected
  • >=0.9.0 <1.0.10
  • >=1.1.0 <1.1.7
  • >=1.2.0.dev0 <1.2.4
  • >=1.3.0.dev0 <1.3.0rc8
Fixed in
  • 1.0.10
  • 1.1.7
  • 1.2.4
  • 1.3.0rc8

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

AI-suggested

Upgrade djust to versions 1.0.10, 1.1.7, 1.2.4, or 1.3.0rc8 or later where the vulnerability is fixed. Since the MCP server communicates only over local stdio and runs as the same OS user, restricting local access to trusted users also mitigates risk.

Description

### 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

References

Community reviews

Loading reviews…