CVE-2026-113731

CVE-2026-113731 is a low-severity path traversal vulnerability in djust (pip), affecting versions >= 0.9.0, <= 1.0.9. It is fixed in 1.0.10, 1.1.7, 1.2.4, 1.3.0rc8.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

djust: Arbitrary absolute-path file read in the findhandlersfor_template MCP tool

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.

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.

log:
10  14:53:20.185935 openat(AT_FDCWD, "/case/project/templates/nonexistent_inside_project.html", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
10  14:53:20.187205 write(1, "{\"error\": \"Could not resolve tem"..., 204) = 204
   ...
10  14:53:20.191831 write(1, "=== ATTACK: absolute path OUTSID"..., 57) = 57
10  14:53:20.194864 write(1, "target path:", 12) = 12
10  14:53:20.198521 write(1, "/case/outside/evil_content.html", 31) = 31
10  14:53:20.202290 newfstatat(AT_FDCWD, "/case/outside/evil_content.html", {st_mode=S_IFREG|0777, st_size=185, ...}, 0) = 0
10  14:53:20.203762 write(1, "exists on disk (from the contain"..., 42) = 42
10  14:53:20.207224 write(1, "True", 4) = 4
10  14:53:20.210392 newfstatat(AT_FDCWD, "/case/outside/evil_content.html", {st_mode=S_IFREG|0777, st_size=185, ...}, 0) = 0
10  14:53:20.211721 newfstatat(AT_FDCWD, "/case/outside/evil_content.html", {st_mode=S_IFREG|0777, st_size=185, ...}, 0) = 0
10  14:53:20.213284 openat(AT_FDCWD, "/case/outside/evil_content.html", O_RDONLY|O_CLOEXEC) = 3
10  14:53:20.215202 newfstatat(3, "", {st_mode=S_IFREG|0777, st_size=185, ...}, AT_EMPTY_PATH) = 0
10  14:53:20.216994 ioctl(3, TCGETS, 0x7ffd68e26aa0) = -1 ENOTTY (Inappropriate ioctl for device)
10  14:53:20.221797 read(3, "<html><body>\n<!-- This file live"..., 186) = 185
10  14:53:20.223610 read(3, "", 1) = 0
10  14:53:20.224913 close(3) = 0

Maintainer assessment

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.

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.

Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files. Typical impact: unauthorized file read or write outside the intended directory.

CVE-2026-113731 has a CVSS score of 3.3 (Low). The vector is requires local access, low privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (1.0.10, 1.1.7, 1.2.4, 1.3.0rc8); upgrading removes the vulnerable code path.

Affected versions

djust (>= 0.9.0, <= 1.0.9) djust (>= 1.1.0, <= 1.1.6) djust (>= 1.2.0.dev0, <= 1.2.3) djust (>= 1.3.0.dev0, <= 1.3.0rc7)

Security releases

djust → 1.0.10 (pip) djust → 1.1.7 (pip) djust → 1.2.4 (pip) djust → 1.3.0rc8 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

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

Frequently Asked Questions

  1. What is CVE-2026-113731? CVE-2026-113731 is a low-severity path traversal vulnerability in djust (pip), affecting versions >= 0.9.0, <= 1.0.9. It is fixed in 1.0.10, 1.1.7, 1.2.4, 1.3.0rc8. Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files.
  2. How severe is CVE-2026-113731? CVE-2026-113731 has a CVSS score of 3.3 (Low). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of djust are affected by CVE-2026-113731? djust (pip) versions >= 0.9.0, <= 1.0.9 is affected.
  4. Is there a fix for CVE-2026-113731? Yes. CVE-2026-113731 is fixed in 1.0.10, 1.1.7, 1.2.4, 1.3.0rc8. Upgrade to this version or later.
  5. Is CVE-2026-113731 exploitable, and should I be worried? Whether CVE-2026-113731 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-113731 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-113731?
    • Upgrade djust to 1.0.10 or later
    • Upgrade djust to 1.1.7 or later
    • Upgrade djust to 1.2.4 or later
    • Upgrade djust to 1.3.0rc8 or later

Other vulnerabilities in djust

Stop the waste.
Protect your environment with Kodem.