T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- scripts/analyst_agent.py:145
- Finding
- API-Controlled Path Traversal Allows Unauthorized Local JSON File Reads<![CDATA[ ## Vulnerability Details **File Location**: `scripts/analyst_agent.py`, lines 145–151 **Vulnerability Type**: Path traversal through an unvalidated API-controlled filename **Risk Level**: Medium ### Vulnerable Code ```python scores_dir = WORKSPACE / "projects" / "hybrid-control-plane" / "data" / "scores" score_file = scores_dir / f"{key}.json" if score_file.exists(): try: with open(score_file) as f: records = json.load(f) return [round(r["score"], 4) for r in records[-10:]] ``` ### Technical Analysis The `key` value originates from a key in the `confidence` object returned by the local `/status` API endpoint. It is passed to `_get_last_10_scores()` and incorporated directly into a filesystem path without validating its characters or verifying that the resolved path remains inside the intended score directory. Python `pathlib` path joining does not prevent traversal. A value containing `../` components can escape the score directory, while an absolute value can cause the preceding base path to be discarded. The script then appends `.json`, opens the resulting path, parses it, and copies up to ten `score` values into `FINDINGS.md`. Successful exploitation requires the targeted file to be readable by the process, have a `.json` suffix under the constructed path, contain valid JSON, and have the expected list-of-records structure with `score` fields. ### Attack Path 1. An attacker compromises, impersonates, or otherwise controls the unauthenticated service listening on `localhost:8765`. 2. The attacker returns a `/status` response whose `confidence` object contains a crafted key with traversal components or an absolute pathname. 3. The attacker supplies values that cause `check_milestones()` to generate a milestone event for that key. 4. `update_findings()` passes the attacker-controlled key to `_get_last_10_scores()`. 5. `_get_last_10_scores()` resolves the crafted value without containment validation and opens the ...[truncated 693 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Restrict score keys to a strict identifier allowlist, such as letters, digits, underscores, and hyphens. - Reject absolute paths, path separators, `.` components, and `..` components. - Resolve both the score directory and candidate path, then verify containment before opening the file. - Validate the loaded JSON against an explicit schema before processing it. - Authenticate the local API or otherwise verify that responses originate from the expected control-plane service. - Run the agent under a dedicated account with access only to the required workspace files. Example containment hardening: ```python import re def _get_last_10_scores(status: dict, key: str) -> list: if not re.fullmatch(r"[A-Za-z0-9_-]+", key): log("WARNING: Rejected invalid score key") return [] scores_dir = ( WORKSPACE / "projects" / "hybrid-control-plane" / "data" / "scores" ).resolve() score_file = (scores_dir / f"{key}.json").resolve() if score_file.parent != scores_dir: log("WARNING: Rejected score path outside score directory") return [] try: with score_file.open() as f: records = json.load(f) if not isinstance(records, list): return [] return [ round(float(record["score"]), 4) for record in records[-10:] if isinstance(record, dict) and "score" in record ] except (FileNotFoundError, OSError, ValueError, TypeError, json.JSONDecodeError): return [] ``` ]]>
