T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/kb_tool.py:1460
- Finding
- Server-Side Request Forgery Through Unrestricted Note-Controlled URL Fetching<![CDATA[ ## Vulnerability Details **File Location**: `scripts/kb_tool.py:1460-1472`, `scripts/kb_tool.py:1608-1630`, and `scripts/kb_tool.py:1725-1789` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code The network-fetching function accepts an arbitrary URL and passes it directly to `urllib.request.urlopen`: ```python def fetch_web_document(url: str, config: Config) -> Dict[str, Any]: if not config.enable_network: return {"status": "failed", "url": url, "text": "", "error": "network disabled"} request = urllib.request.Request( url, headers={ "User-Agent": DEFAULT_USER_AGENT, "Accept": "text/html,application/xhtml+xml,text/plain,application/json;q=0.8,*/*;q=0.5", }, ) try: with urllib.request.urlopen(request, timeout=config.network_timeout) as response: raw = response.read(250000) ``` HTTP URLs extracted from notes become source candidates without destination validation: ```python def source_candidates_for_note(note: NormalizedNote, vault_root: pathlib.Path) -> List[Dict[str, str]]: candidates: List[Dict[str, str]] = [] seen = set() for link in dedupe_keep_order(([note.original_url] if note.original_url else []) + note.source_links): if not link or link in seen: continue seen.add(link) if link.startswith("obsidian://"): resolved = resolve_obsidian_uri_to_path(link, vault_root) if resolved: candidates.append({"kind": "obsidian_note", "value": str(resolved.resolve()), "raw": link}) else: candidates.append({"kind": "obsidian_note", "value": "", "raw": link}) elif link.startswith("http"): kind = "source_url" if link == note.original_url else "embedded_url" candidates.append({"kind": kind, "value": link, "raw": link}) return candidates ``` The resulting candidate is fetched ...[truncated 3881 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Default to no network access** - Change `research.enable_network` to `false`. - Require explicit user opt-in for each build or trusted vault. 2. **Restrict protocols** - Permit HTTPS only unless HTTP is explicitly required and approved. - Reject URLs containing user information or unsupported schemes. 3. **Validate resolved destinations** - Resolve all hostnames before connecting. - Reject IPv4 and IPv6 loopback, private, link-local, multicast, unspecified, and reserved ranges. - Revalidate immediately before connection to reduce DNS-rebinding risk. 4. **Validate redirects** - Disable automatic redirects or implement a redirect handler that validates every target. - Apply the same scheme, hostname, IP-range, and port policy to each redirect. - Enforce a small redirect limit. 5. **Restrict ports and domains** - Allow only ports 443 and, if strictly necessary, 80. - Prefer an explicit trusted-domain allowlist. - Require confirmation before contacting a domain first observed in note content. 6. **Isolate network enrichment** - Run fetching in a sandbox or network namespace without access to localhost, private networks, or metadata services. - Use an egress proxy with destination filtering. 7. **Limit retained response data** - Avoid persisting raw internal responses. - Record only approved excerpts after destination and content validation. - Clearly mark the provenance of remotely fetched content. 8. **Add regression tests** - Verify rejection of `127.0.0.1`, `::1`, RFC 1918 addresses, link-local ranges, integer/encoded IP representations, and redirects to prohibited destinations. ]]>
