T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/research.py:259
- Finding
- Stored HTML and Script Injection in Generated Research Reports## Vulnerability Details **File Location**: `scripts/research.py`, lines 259–316 **Vulnerability Type**: Unescaped untrusted content in HTML output **Risk Level**: Medium ### Vulnerable Code ```python html = f"""<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Research Report: {config.question[:80]}</title> <style> body {{ font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }} h1 {{ color: #1a1a2e; }} h2 {{ border-bottom: 2px solid #eee; padding-bottom: 0.3rem; }} .meta {{ color: #666; margin-bottom: 1rem; }} .quality {{ color: {color}; font-weight: bold; }} table {{ border-collapse: collapse; width: 100%; margin: 1rem 0; }} th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }} th {{ background: #f5f5f5; }} .source {{ font-size: 0.9em; color: #555; }} </style> </head> <body> <h1>Research Report</h1> <p class="meta"> <strong>Question:</strong> {config.question}<br> <strong>Date:</strong> {config.date} | <strong>Duration:</strong> ~{config.duration}s | <strong>Quality:</strong> <span class="quality">{avg_quality:.1f}/1.0</span> </p> <h2>Executive Summary</h2> <ul> {''.join([f'<li>{f.get("summary", "No summary.")}' for f in findings[:5]])} </ul> <h2>Key Findings</h2> """ for i, f in enumerate(findings, 1): score = quality_scores[i - 1] if i <= len(quality_scores) else 0 html += f""" <div style="margin: 1rem 0; padding: 1rem; border-left: 3px solid #4a90d9; background: #fafafa;"> <strong>{i}. {f.get('title', f'Finding {i}')} </strong> <span class="quality">({score:.1f})</span> <p>{f.get('details', 'No details.')}</p> </div> """ html += f""" <h2>Quality Assessment</h2> <table> <tr><th>Metric</th><th>Value</th></tr> <tr><td>Average source quality</td><td>{avg_quality:.1f}/1.0</td></tr> <tr><td>Sources after dedup</td><td>{len(sources)}</td></tr> <tr><td>Follow-up rounds</td><td>{config.followups}</td></tr> </table> <h2>Limitations</h2> <ul> <li> ...[truncated 3124 chars]
- Remediation
- ## Remediation Suggestions 1. Apply contextual HTML escaping to every dynamic text value before interpolation: ```python from html import escape question = escape(str(config.question), quote=True) summary = escape(str(f.get("summary", "No summary.")), quote=True) title = escape(str(f.get("title", f"Finding {i}")), quote=True) details = escape(str(f.get("details", "No details.")), quote=True) source_title = escape(str(s.get("title", "Unknown")), quote=True) ``` 2. Validate source URLs with `urllib.parse.urlparse()` and allow only explicitly supported schemes, preferably `https`. Reject dangerous or unexpected schemes such as `javascript:`, `data:`, and `file:`. 3. Render source URLs as properly encoded anchor attributes rather than raw text: ```python from html import escape from urllib.parse import urlparse def safe_https_url(value: str) -> str: value = str(value) parsed = urlparse(value) if parsed.scheme != "https" or not parsed.netloc: return "" return escape(value, quote=True) ``` 4. Prefer a template engine with automatic escaping enabled instead of assembling HTML through f-strings. 5. If reports are hosted, add a restrictive Content Security Policy that disallows inline scripts and limits resource origins. This should be defense in depth rather than a substitute for escaping. 6. Add regression tests covering payloads in every dynamic field, including: ```html <script>alert(1)</script> <img src=x onerror=alert(1)> </title><script>alert(1)</script> ``` Tests should verify that these values appear only as encoded text and never create executable DOM elements. 7. Document that fetched web content is untrusted and must not be interpreted as markup or executable instructions at any stage of report generation.
