T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/sinks/_md.py:40
- Finding
- Arbitrary Local File Disclosure Through Markdown Image References<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sinks/_md.py:40-56`; exploitation sink at `scripts/sinks/linear.py:33-60` **Vulnerability Type**: Unrestricted local file read and external disclosure **Risk Level**: High ### Vulnerable Code `scripts/sinks/_md.py:40-56`: ```python def find_local_images(markdown: str, base_dir) -> list: """Return ``[(alt, ref, Path)]`` for image refs that point at existing local files.""" base = Path(base_dir) if base_dir else None found = [] seen = set() for match in _IMAGE_RE.finditer(markdown): ref = match.group("ref") if is_remote(ref) or ref in seen: continue path = Path(ref) if not path.is_absolute() and base is not None: path = base / ref if path.is_file(): found.append((match.group("alt"), ref, path)) seen.add(ref) return found ``` `scripts/sinks/linear.py:33-60`: ```python def _data_uri(path: Path) -> str: mime = _MIME.get(path.suffix.lower(), "image/png") b64 = base64.b64encode(path.read_bytes()).decode("ascii") return f"data:{mime};base64,{b64}" @register class LinearSink(Sink): name = "linear" requires = ("LINEAR_API_KEY", "LINEAR_TEAM_ID") label = "Linear issue (GraphQL API)" def deliver(self, doc: ParsedDoc) -> SinkResult: key = self.env("LINEAR_API_KEY") team = self.env("LINEAR_TEAM_ID") headers = {"Authorization": key, "Content-Type": "application/json"} base_dir = Path(doc.markdown_path).parent if doc.markdown_path else None images = _md.find_local_images(doc.markdown, base_dir) mapping = {ref: _data_uri(path) for _alt, ref, path in images} body = _md.rewrite_images(doc.markdown, mapping) status, parsed = _http.request_json("POST", API, headers=headers, payload={ "query": _MUTATION, "variables": {"input": { "teamId": team, "title": doc ...[truncated 2250 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject absolute image paths and any path containing parent traversal. 2. Resolve both the output directory and candidate path before reading: ```python base = Path(base_dir).resolve() candidate = (base / ref).resolve() try: candidate.relative_to(base) except ValueError: continue if candidate.is_file(): found.append((alt, ref, candidate)) ``` 3. On supported Python versions, use `candidate.is_relative_to(base)` after canonicalization. 4. Permit only expected generated asset directories, such as `<output>/images/`. 5. Apply an allowlist of supported image extensions and verify file signatures rather than trusting the filename. 6. Reject symbolic links or confirm that their resolved targets remain under the authorized directory. 7. Add file-size limits before reading and Base64-encoding assets. 8. Require explicit confirmation before a sink embeds any local file. 9. Add regression tests for absolute paths, `../` traversal, symlinks, encoded traversal, and paths sharing only a lexical prefix with the allowed directory. ]]>
