Threats tagged 'ghsa-r57g-mc79-8hj2'
1 threat
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Low | 3.3 | EPSS 0.2% | CVE-2026-113731djust · Path Traversal Low3.3EPSS 0.2%CVE Database V5CVE-2026-113731 ### 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 Database V5 |
Showing 1–1 of 1 result