T09 · Insecure Skill Coding Practices
- Location
scripts/md2epub.py:45- Finding
EPUB Asset Path Traversal Permits Local File Disclosure
- Content
View full analysis
set[str]: return {src for body in chapters_html for src in IMG_SRC_RE.findall(body)} ``` ```python needed = collect_images(bodies) base = md_path.parent missing = sorted(s for s in needed if not (base / s).exists()) ``` ```python for i, src in enumerate(sorted(needed - set(missing))): p = base / src mime = mimetypes.guess_type(p.name)[0] or "application/octet-stream" book.add_item(epub.EpubImage( uid=f"img{i:03d}", file_name=src, media_type=mime, content=p.read_bytes() )) ``` ### Technical Analysis The image-source regular expression only verifies that a generated HTML image path starts with `assets/`. It does not reject `..` path components, absolute-path transformations, or symbolic links that escape the intended asset directory. A source such as `assets/../../sensitive-file` satisfies the regular expression. Joining that value to `md_path.parent` does not constrain it to the `assets` directory. Python resolves the traversal when `exists()` and `read_bytes()` access the filesystem. Consequently, attacker-controlled or untrusted Markdown can cause the EPUB generator to read a file outside the project asset directory and embed its contents in the resulting EPUB. The EPUB then acts as a local-file disclosure artifact. ### Attack Path 1. An attacker supplies or modifies a Markdown input processed by `md2epub.py`. 2. The Markdown contains an image reference such as: ```markdown  ``` 3. Markdown conversion emits an HTML image source beginning with `assets/`. 4. `IMG_SRC_RE` accepts the traversal path. 5. `(base / src).exists()` chec ...[truncated 1012 chars]- Remediation
View remediation
Path | None: if not src.startswith("assets/"): return None candidate = (base / src).resolve() try: candidate.relative_to(assets_root) except ValueError: return None if not candidate.is_file(): return None return candidate ``` Use the validated path for all filesystem operations: ```python validated_assets = {} for src in needed: candidate = resolve_asset(src) if candidate is None: warnings.append(f"Rejected unsafe or missing image: {src}") continue validated_assets[src] = candidate ``` Additional hardening should include: - Rejecting all paths containing `..` components before resolution. - Rejecting absolute paths and URI schemes. - Ensuring symbolic links cannot resolve outside `assets_root`. - Generating sanitized EPUB member names rather than reusing untrusted source paths. - Applying the same validation before both existence checks and file reads. - Adding tests for `assets/../`, `assets/../../`, absolute paths, encoded traversal, and escaping symbolic links. ]]>
