T09 · Insecure Skill Coding Practices
- Location
scripts/research.py:259- Finding
Stored HTML and Script Injection in Generated Research Reports
- Content
View full analysis
Vulnerability Details
File Location:
scripts/research.py, lines 259–316
Vulnerability Type: Unescaped untrusted content in HTML output
Risk Level: MediumVulnerable 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
View remediation
Remediation Suggestions
-
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) -
Validate source URLs with
urllib.parse.urlparse()and allow only explicitly supported schemes, preferablyhttps. Reject dangerous or unexpected schemes such asjavascript:,data:, andfile:. -
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) -
Prefer a template engine with automatic escaping enabled instead of assembling HTML through f-strings.
-
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.
-
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.
-
Document that fetched web content is untrusted and must not be interpreted as markup or executable instructions at any stage of report generation.
-
