diff --git a/.claude/skills/design-system/scripts/generate-slide.py b/.claude/skills/design-system/scripts/generate-slide.py
index 228a50a..2de390d 100644
--- a/.claude/skills/design-system/scripts/generate-slide.py
+++ b/.claude/skills/design-system/scripts/generate-slide.py
@@ -8,9 +8,26 @@ NO hardcoded colors, fonts, or spacing allowed
import argparse
import json
+from html import escape
from pathlib import Path
from datetime import datetime
+
+def _e(value, default=''):
+ """HTML-escape a user-supplied value for safe embedding in HTML content."""
+ return escape(str(value if value is not None else default))
+
+
+def _safe_url(url, default='#'):
+ """Validate and escape a URL for use in href attributes.
+
+ Only allows http://, https://, #, and / schemes to prevent
+ javascript: URI injection (CWE-79).
+ """
+ if url and str(url).strip().lower().startswith(('http://', 'https://', '#', '/')):
+ return escape(str(url), quote=True)
+ return default
+
# Paths
SCRIPT_DIR = Path(__file__).parent
DATA_DIR = SCRIPT_DIR.parent / "data"
@@ -412,16 +429,16 @@ def generate_title_slide(data):
"""Title slide with gradient headline"""
return f'''
- {data.get('badge', 'Pitch Deck')}
- {data.get('title', 'Your Title Here')}
- {data.get('subtitle', 'Your compelling subtitle')}
+ {_e(data.get('badge', 'Pitch Deck'))}
+ {_e(data.get('title', 'Your Title Here'))}
+ {_e(data.get('subtitle', 'Your compelling subtitle'))}
'''
@@ -432,27 +449,27 @@ def generate_problem_slide(data):
return f'''
The Problem
- {data.get('headline', 'The problem your audience faces')}
+ {_e(data.get('headline', 'The problem your audience faces'))}
01
-
{data.get('pain_1_title', 'Pain Point 1')}
-
{data.get('pain_1_desc', 'Description of the first pain point')}
+
{_e(data.get('pain_1_title', 'Pain Point 1'))}
+
{_e(data.get('pain_1_desc', 'Description of the first pain point'))}
02
-
{data.get('pain_2_title', 'Pain Point 2')}
-
{data.get('pain_2_desc', 'Description of the second pain point')}
+
{_e(data.get('pain_2_title', 'Pain Point 2'))}
+
{_e(data.get('pain_2_desc', 'Description of the second pain point'))}
03
-
{data.get('pain_3_title', 'Pain Point 3')}
-
{data.get('pain_3_desc', 'Description of the third pain point')}
+
{_e(data.get('pain_3_title', 'Pain Point 3'))}
+
{_e(data.get('pain_3_desc', 'Description of the third pain point'))}
'''
@@ -463,28 +480,28 @@ def generate_solution_slide(data):
return f'''
The Solution
- {data.get('headline', 'How we solve this')}
+ {_e(data.get('headline', 'How we solve this'))}
✓
-
{data.get('feature_1_title', 'Feature 1')}
-
{data.get('feature_1_desc', 'Description of feature 1')}
+
{_e(data.get('feature_1_title', 'Feature 1'))}
+
{_e(data.get('feature_1_desc', 'Description of feature 1'))}
✓
-
{data.get('feature_2_title', 'Feature 2')}
-
{data.get('feature_2_desc', 'Description of feature 2')}
+
{_e(data.get('feature_2_title', 'Feature 2'))}
+
{_e(data.get('feature_2_desc', 'Description of feature 2'))}
✓
-
{data.get('feature_3_title', 'Feature 3')}
-
{data.get('feature_3_desc', 'Description of feature 3')}
+
{_e(data.get('feature_3_title', 'Feature 3'))}
+
{_e(data.get('feature_3_desc', 'Description of feature 3'))}
@@ -496,8 +513,8 @@ def generate_solution_slide(data):
'''
@@ -514,21 +531,21 @@ def generate_metrics_slide(data):
metrics_html = ''.join([f'''
-
{m['value']}
-
{m['label']}
+
{_e(m.get('value', ''))}
+
{_e(m.get('label', ''))}
''' for m in metrics[:4]])
return f'''
Traction
- {data.get('headline', 'Our Growth')}
+ {_e(data.get('headline', 'Our Growth'))}
{metrics_html}
'''
@@ -544,25 +561,25 @@ def generate_chart_slide(data):
])
bars_html = ''.join([f'''
-
-
{b.get('display', str(b['value']) + '%')}
-
{b['label']}
+
+ {_e(b.get('display', str(b.get('value', 0)) + '%'))}
+ {_e(b.get('label', ''))}
''' for b in bars])
return f'''
- {data.get('badge', 'Growth')}
- {data.get('headline', 'Revenue Growth')}
+ {_e(data.get('badge', 'Growth'))}
+ {_e(data.get('headline', 'Revenue Growth'))}
-
{data.get('chart_title', 'Quarterly Revenue')}
+
{_e(data.get('chart_title', 'Quarterly Revenue'))}
{bars_html}
'''
@@ -574,13 +591,13 @@ def generate_testimonial_slide(data):
What They Say
-
"{data.get('quote', 'This product changed how we work. Incredible results.')}"
-
{data.get('author', 'Jane Doe')}
-
{data.get('role', 'CEO, Example Company')}
+
"{_e(data.get('quote', 'This product changed how we work. Incredible results.'))}"
+
{_e(data.get('author', 'Jane Doe'))}
+
{_e(data.get('role', 'CEO, Example Company'))}
'''
@@ -590,14 +607,14 @@ def generate_cta_slide(data):
"""Closing CTA slide"""
return f'''
- {data.get('headline', 'Ready to get started?')}
- {data.get('subheadline', 'Join thousands of teams already using our solution.')}
+ {_e(data.get('headline', 'Ready to get started?'))}
+ {_e(data.get('subheadline', 'Join thousands of teams already using our solution.'))}
'''
@@ -632,7 +649,7 @@ def generate_deck(slides_data, title="Pitch Deck"):
tokens_rel_path = "../../../assets/design-tokens.css"
return SLIDE_TEMPLATE.format(
- title=title,
+ title=escape(str(title)),
tokens_css_path=tokens_rel_path,
slides_content=slides_html
)