|
5 | 5 |
|
6 | 6 | from __future__ import annotations |
7 | 7 |
|
| 8 | +import html |
8 | 9 | from pathlib import Path |
9 | 10 | from typing import Any |
10 | 11 |
|
@@ -77,13 +78,15 @@ def add_header( |
77 | 78 | Returns: |
78 | 79 | Self for chaining |
79 | 80 | """ |
80 | | - subtitle_html = f'<p style="font-size: 0.85rem; color: var(--oa-text-secondary);">{subtitle}</p>' if subtitle else "" |
| 81 | + # Escape title and subtitle for XSS prevention |
| 82 | + safe_title = html.escape(title) |
| 83 | + subtitle_html = f'<p style="font-size: 0.85rem; color: var(--oa-text-secondary);">{html.escape(subtitle)}</p>' if subtitle else "" |
81 | 84 |
|
82 | 85 | self._header_html = f''' |
83 | 86 | <header style="padding: 16px 24px; background: var(--oa-bg-secondary); border-bottom: 1px solid var(--oa-border-color); margin-bottom: 24px;"> |
84 | 87 | <div style="display: flex; align-items: center; justify-content: space-between;"> |
85 | 88 | <div> |
86 | | - <h1 style="font-size: 1.25rem; font-weight: 600; margin: 0;">{title}</h1> |
| 89 | + <h1 style="font-size: 1.25rem; font-weight: 600; margin: 0;">{safe_title}</h1> |
87 | 90 | {subtitle_html} |
88 | 91 | </div> |
89 | 92 | <div style="display: flex; align-items: center; gap: 16px;"> |
@@ -128,7 +131,8 @@ def add_section( |
128 | 131 | Self for chaining |
129 | 132 | """ |
130 | 133 | extra_class = f" {class_name}" if class_name else "" |
131 | | - title_html = f'<h2 style="font-size: 1.1rem; font-weight: 600; margin-bottom: 16px;">{title}</h2>' if title else "" |
| 134 | + # Escape title for XSS prevention |
| 135 | + title_html = f'<h2 style="font-size: 1.1rem; font-weight: 600; margin-bottom: 16px;">{html.escape(title)}</h2>' if title else "" |
132 | 136 |
|
133 | 137 | self._sections.append(f''' |
134 | 138 | <section class="oa-section{extra_class}" style="margin-bottom: 24px;"> |
@@ -216,12 +220,15 @@ def render(self) -> str: |
216 | 220 | </script> |
217 | 221 | ''' |
218 | 222 |
|
| 223 | + # Escape title for XSS prevention |
| 224 | + safe_title = html.escape(self.title) |
| 225 | + |
219 | 226 | return f'''<!DOCTYPE html> |
220 | 227 | <html lang="en"> |
221 | 228 | <head> |
222 | 229 | <meta charset="UTF-8"> |
223 | 230 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
224 | | - <title>{self.title}</title> |
| 231 | + <title>{safe_title}</title> |
225 | 232 | {scripts_html} |
226 | 233 | <style> |
227 | 234 | {core_css} |
|
0 commit comments