T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/get_transcript.py:143
- Finding
- Path Traversal in Transcript Loading Allows Local Text File Disclosure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/get_transcript.py`, lines 143–149 and 312–326 **Vulnerability Type**: Path traversal and unauthorized local file read **Risk Level**: High ### Vulnerable Code ```python def load_transcript(video_id: str) -> Optional[str]: path = DATA_DIR / f"{video_id}.txt" if not path.exists(): return None return path.read_text(encoding="utf-8") ``` The untrusted value is obtained from the `ask` command and passed directly to the vulnerable function: ```python def cmd_ask(args): video_id = args.video_id if video_id in ("ACTIVE_VIDEO", "-", ""): video_id = get_active_video() if not video_id: print("❌ No active video in session.") sys.exit(1) transcript = load_transcript(video_id) if not transcript: print("❌ Transcript not found.") sys.exit(1) chunks = retrieve_chunks(transcript, args.question) ``` ### Technical Analysis The `ask` command treats `video_id` as a filename component without validating that it is a legitimate 11-character YouTube video ID. The expression: ```python DATA_DIR / f"{video_id}.txt" ``` does not prevent absolute paths, directory separators, or `..` traversal components. The resulting path is also not resolved and checked to ensure that it remains inside `DATA_DIR`. An attacker who can cause the skill to invoke the `ask` command can therefore provide a value such as `../../../../tmp/confidential`. The application appends `.txt`, resolves the traversal through normal filesystem semantics, and reads the resulting file if it exists and is valid UTF-8. The file content is subsequently processed by `retrieve_chunks()` and printed. This limits each invocation to selected chunks but does not prevent disclosure; questions can be adjusted to retrieve different portions of the target file. ### Attack Path 1. The attacker identifies or predicts a readable text file on the host whose file ...[truncated 1234 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Validate every supplied video ID before using it as a filesystem component: ```python VIDEO_ID_RE = re.compile(r"^[A-Za-z0-9_-]{11}$") def validate_video_id(video_id: str) -> str: if not VIDEO_ID_RE.fullmatch(video_id): raise ValueError("Invalid video ID") return video_id ``` Apply validation to both direct `ask` arguments and values loaded from session state. Add defense-in-depth containment checks: ```python def load_transcript(video_id: str) -> Optional[str]: video_id = validate_video_id(video_id) data_root = DATA_DIR.resolve() path = (data_root / f"{video_id}.txt").resolve() if path.parent != data_root: raise ValueError("Transcript path escapes the data directory") if not path.is_file(): return None return path.read_text(encoding="utf-8") ``` Additional hardening measures: - Reject absolute paths, path separators, null bytes, and traversal components. - Validate video IDs before writing them to `session.json`. - Treat persisted session data as untrusted when loading it. - Run the skill under a dedicated low-privilege account. - Add tests covering `../`, absolute paths, nested paths, malformed IDs, and manipulated session data. ]]>
