T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/build_report_html.py:895
- Finding
- Unsafe Markdown Link Schemes Permit JavaScript Execution in Generated Reports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/build_report_html.py`, lines 895–899 **Vulnerability Type**: Unsafe URI scheme handling in Markdown links **Risk Level**: Medium ### Vulnerable Code ```python text = stash( re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), text, lambda match: f'<a href="{escape(match.group(2), quote=True)}">{escape(match.group(1))}</a>', ) ``` ### Technical Analysis The Markdown renderer places a user-controlled link destination directly into an HTML `href` attribute. Although `escape(..., quote=True)` prevents HTML attribute breakout, it does not validate the URI scheme. Consequently, dangerous schemes such as `javascript:`, `data:`, `vbscript:`, or `file:` can survive HTML rendering. For example, the following Markdown contains a JavaScript URL that does not require parentheses: ```markdown [Open report details](javascript:document.body.textContent='Report modified') ``` It is rendered as: ```html <a href="javascript:document.body.textContent='Report modified'">Open report details</a> ``` When a reader clicks the link, the browser evaluates the JavaScript in the generated report's document context. This is a scheme-validation flaw rather than an HTML-escaping flaw: encoding special characters does not neutralize a syntactically valid dangerous URI. The vulnerable data flow is: 1. Markdown is read from a file under the report's `sections/` directory. 2. `render_markdown()` processes the section. 3. `render_inline()` recognizes Markdown links. 4. The destination is HTML-escaped but not parsed or allowlisted. 5. The resulting unsafe link is written to `dist/report.html`. ### Attack Path 1. An attacker causes crafted Markdown to be included in a report section. This may occur through externally supplied report content, chart-related text incorporated into a generated section, or modification of a section file before report generation. 2. The attacker includes a link with a dangerous destination, such ...[truncated 1331 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Parse each destination before rendering it and enforce an explicit scheme allowlist. 1. Permit only necessary schemes, such as: - `https` - `http` - `mailto`, if required 2. Permit relative paths and fragment identifiers only if the report format needs them. 3. Reject or render as plain text all other schemes, including: - `javascript` - `data` - `vbscript` - `file` 4. Normalize and inspect the destination before validation: - Strip leading and trailing whitespace. - Reject embedded control characters. - Compare schemes case-insensitively. - Account for percent-encoded and HTML-entity-obfuscated schemes. 5. Add `rel="noopener noreferrer"` to external links. 6. Add a restrictive Content Security Policy as defense in depth, including `script-src 'none'`. Scheme allowlisting should remain the primary control. 7. Add regression tests for mixed-case schemes, leading whitespace, control characters, percent encoding, HTML entities, protocol-relative URLs, and valid relative links. A hardened implementation can use `urllib.parse.urlsplit`: ```python from urllib.parse import urlsplit ALLOWED_SCHEMES = {"http", "https", "mailto"} def safe_href(raw: str) -> str | None: value = raw.strip() if any(ord(char) < 0x20 or ord(char) == 0x7F for char in value): return None parsed = urlsplit(value) if parsed.scheme and parsed.scheme.lower() not in ALLOWED_SCHEMES: return None if value.startswith("//"): return None return value ``` The renderer should emit a normal anchor only when `safe_href()` returns a value; otherwise, it should render the label as escaped plain text. ]]>
