T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/generate_postmortem.py:383
- Finding
- Stored HTML and JavaScript Injection in Generated Reports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate_postmortem.py`, lines 383-466 **Vulnerability Type**: Stored HTML injection / cross-site scripting in generated HTML reports **Risk Level**: Medium ### Vulnerable Code ```python def generate_html(markdown_content, title): """Wrap markdown content in a simple HTML template.""" # Simple markdown-to-HTML conversion for key elements html = markdown_content # Headers html = re.sub(r'^# (.+)$', r'<h1>\1</h1>', html, flags=re.MULTILINE) html = re.sub(r'^## (.+)$', r'<h2>\1</h2>', html, flags=re.MULTILINE) html = re.sub(r'^### (.+)$', r'<h3>\1</h3>', html, flags=re.MULTILINE) # Bold html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html) # Italic html = re.sub(r'_(.+?)_', r'<em>\1</em>', html) # Code html = re.sub(r'`(.+?)`', r'<code>\1</code>', html) # Lists html = re.sub(r'^- (.+)$', r'<li>\1</li>', html, flags=re.MULTILINE) # Tables (simple conversion) def convert_table(match): lines = match.group(0).strip().split('\n') rows = [] for i, line in enumerate(lines): if '---' in line: continue cells = [c.strip() for c in line.strip('|').split('|')] tag = 'th' if i == 0 else 'td' row = ''.join(f'<{tag}>{c}</{tag}>' for c in cells) rows.append(f'<tr>{row}</tr>') return f'<table>{"".join(rows)}</table>' html = re.sub(r'(\|.+\|(?:\n\|.+\|)*)', convert_table, html) # Paragraphs (lines not already wrapped) lines = html.split('\n') processed = [] for line in lines: if line.strip() and not line.strip().startswith('<') and not line.strip().startswith('*'): processed.append(f'<p>{line}</p>') else: processed.append(line) html = '\n'.join(processed) return f"""<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="wid ...[truncated 3452 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. HTML-escape every untrusted scalar before inserting it into HTML, including the title, incident metadata, timeline event text, action items, and log messages. Use `html.escape(value, quote=True)` where direct interpolation is unavoidable. 2. Replace the regular-expression Markdown conversion with a maintained Markdown renderer configured to disable or escape raw HTML. 3. If raw HTML must be supported, sanitize the rendered output with a strict allowlist. Remove at minimum: - `<script>`, `<iframe>`, `<object>`, `<embed>`, and similar active elements. - Event-handler attributes such as `onclick`, `onerror`, and `onload`. - Dangerous URL schemes such as `javascript:` and unsafe `data:` URLs. - Unexpected SVG or MathML elements and attributes. 4. Construct the document title from an escaped value: ```python import html safe_title = html.escape(str(title), quote=True) ``` 5. Treat log messages as plain text rather than markup, because logs commonly contain attacker-controlled request data. 6. Consider adding a restrictive Content Security Policy to generated reports as defense in depth: ```html <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:"> ``` This should supplement output encoding and sanitization, not replace them. 7. Add regression tests covering payloads in every input channel, including: - Incident titles and summaries. - Timeline event text. - Action-item table cells. - Parsed log messages. - Closing-tag payloads such as `</title>`. - Event handlers and dangerous URL schemes. The tests should verify that payloads appear only as encoded text and cannot create executable DOM elements or attributes. ]]>
