fix: HTML-escape all user data in slide generator to prevent XSS (#274)

Add html.escape() to all 46 user-controlled data.get() calls across
all 7 slide generator functions (title, problem, solution, metrics,
chart, testimonial, cta) and the deck title.

Add URL scheme validation for cta_url href to block javascript: URI
injection.

Closes #247
This commit is contained in:
Artemii Fridriksen 2026-06-22 14:17:29 -04:00 committed by GitHub
parent 9bb646b336
commit d457006301
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -8,9 +8,26 @@ NO hardcoded colors, fonts, or spacing allowed
import argparse import argparse
import json import json
from html import escape
from pathlib import Path from pathlib import Path
from datetime import datetime 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 # Paths
SCRIPT_DIR = Path(__file__).parent SCRIPT_DIR = Path(__file__).parent
DATA_DIR = SCRIPT_DIR.parent / "data" DATA_DIR = SCRIPT_DIR.parent / "data"
@ -412,16 +429,16 @@ def generate_title_slide(data):
"""Title slide with gradient headline""" """Title slide with gradient headline"""
return f''' return f'''
<section class="slide slide--glow flex flex-col items-center justify-center text-center"> <section class="slide slide--glow flex flex-col items-center justify-center text-center">
<div class="badge mb-6">{data.get('badge', 'Pitch Deck')}</div> <div class="badge mb-6">{_e(data.get('badge', 'Pitch Deck'))}</div>
<h1 class="slide-title mb-6">{data.get('title', 'Your Title Here')}</h1> <h1 class="slide-title mb-6">{_e(data.get('title', 'Your Title Here'))}</h1>
<p class="slide-subheading mb-8">{data.get('subtitle', 'Your compelling subtitle')}</p> <p class="slide-subheading mb-8">{_e(data.get('subtitle', 'Your compelling subtitle'))}</p>
<div class="flex gap-4"> <div class="flex gap-4">
<a href="#" class="btn btn-primary">{data.get('cta', 'Get Started')}</a> <a href="#" class="btn btn-primary">{_e(data.get('cta', 'Get Started'))}</a>
<a href="#" class="btn btn-secondary">{data.get('secondary_cta', 'Learn More')}</a> <a href="#" class="btn btn-secondary">{_e(data.get('secondary_cta', 'Learn More'))}</a>
</div> </div>
<div class="slide-footer"> <div class="slide-footer">
<span>{data.get('company', 'Company Name')}</span> <span>{_e(data.get('company', 'Company Name'))}</span>
<span>{data.get('date', datetime.now().strftime('%B %Y'))}</span> <span>{_e(data.get('date', datetime.now().strftime('%B %Y')))}</span>
</div> </div>
</section> </section>
''' '''
@ -432,27 +449,27 @@ def generate_problem_slide(data):
return f''' return f'''
<section class="slide slide--surface"> <section class="slide slide--surface">
<div class="badge mb-6">The Problem</div> <div class="badge mb-6">The Problem</div>
<h2 class="slide-heading mb-8">{data.get('headline', 'The problem your audience faces')}</h2> <h2 class="slide-heading mb-8">{_e(data.get('headline', 'The problem your audience faces'))}</h2>
<div class="grid grid-3 gap-8"> <div class="grid grid-3 gap-8">
<div class="card"> <div class="card">
<div class="text-primary" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">01</div> <div class="text-primary" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">01</div>
<h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{data.get('pain_1_title', 'Pain Point 1')}</h4> <h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{_e(data.get('pain_1_title', 'Pain Point 1'))}</h4>
<p class="text-muted">{data.get('pain_1_desc', 'Description of the first pain point')}</p> <p class="text-muted">{_e(data.get('pain_1_desc', 'Description of the first pain point'))}</p>
</div> </div>
<div class="card"> <div class="card">
<div class="text-secondary" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">02</div> <div class="text-secondary" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">02</div>
<h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{data.get('pain_2_title', 'Pain Point 2')}</h4> <h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{_e(data.get('pain_2_title', 'Pain Point 2'))}</h4>
<p class="text-muted">{data.get('pain_2_desc', 'Description of the second pain point')}</p> <p class="text-muted">{_e(data.get('pain_2_desc', 'Description of the second pain point'))}</p>
</div> </div>
<div class="card"> <div class="card">
<div class="text-accent" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">03</div> <div class="text-accent" style="font-size: var(--primitive-fontSize-4xl); margin-bottom: var(--primitive-spacing-4);">03</div>
<h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{data.get('pain_3_title', 'Pain Point 3')}</h4> <h4 style="margin-bottom: var(--primitive-spacing-2); font-size: var(--primitive-fontSize-xl);">{_e(data.get('pain_3_title', 'Pain Point 3'))}</h4>
<p class="text-muted">{data.get('pain_3_desc', 'Description of the third pain point')}</p> <p class="text-muted">{_e(data.get('pain_3_desc', 'Description of the third pain point'))}</p>
</div> </div>
</div> </div>
<div class="slide-footer"> <div class="slide-footer">
<span>{data.get('company', 'Company Name')}</span> <span>{_e(data.get('company', 'Company Name'))}</span>
<span>{data.get('page', '2')}</span> <span>{_e(data.get('page', '2'))}</span>
</div> </div>
</section> </section>
''' '''
@ -463,28 +480,28 @@ def generate_solution_slide(data):
return f''' return f'''
<section class="slide"> <section class="slide">
<div class="badge mb-6">The Solution</div> <div class="badge mb-6">The Solution</div>
<h2 class="slide-heading mb-8">{data.get('headline', 'How we solve this')}</h2> <h2 class="slide-heading mb-8">{_e(data.get('headline', 'How we solve this'))}</h2>
<div class="flex gap-8" style="flex: 1;"> <div class="flex gap-8" style="flex: 1;">
<div style="flex: 1;"> <div style="flex: 1;">
<div class="feature-item"> <div class="feature-item">
<div class="feature-icon">&#10003;</div> <div class="feature-icon">&#10003;</div>
<div class="feature-content"> <div class="feature-content">
<h4>{data.get('feature_1_title', 'Feature 1')}</h4> <h4>{_e(data.get('feature_1_title', 'Feature 1'))}</h4>
<p>{data.get('feature_1_desc', 'Description of feature 1')}</p> <p>{_e(data.get('feature_1_desc', 'Description of feature 1'))}</p>
</div> </div>
</div> </div>
<div class="feature-item"> <div class="feature-item">
<div class="feature-icon">&#10003;</div> <div class="feature-icon">&#10003;</div>
<div class="feature-content"> <div class="feature-content">
<h4>{data.get('feature_2_title', 'Feature 2')}</h4> <h4>{_e(data.get('feature_2_title', 'Feature 2'))}</h4>
<p>{data.get('feature_2_desc', 'Description of feature 2')}</p> <p>{_e(data.get('feature_2_desc', 'Description of feature 2'))}</p>
</div> </div>
</div> </div>
<div class="feature-item"> <div class="feature-item">
<div class="feature-icon">&#10003;</div> <div class="feature-icon">&#10003;</div>
<div class="feature-content"> <div class="feature-content">
<h4>{data.get('feature_3_title', 'Feature 3')}</h4> <h4>{_e(data.get('feature_3_title', 'Feature 3'))}</h4>
<p>{data.get('feature_3_desc', 'Description of feature 3')}</p> <p>{_e(data.get('feature_3_desc', 'Description of feature 3'))}</p>
</div> </div>
</div> </div>
</div> </div>
@ -496,8 +513,8 @@ def generate_solution_slide(data):
</div> </div>
</div> </div>
<div class="slide-footer"> <div class="slide-footer">
<span>{data.get('company', 'Company Name')}</span> <span>{_e(data.get('company', 'Company Name'))}</span>
<span>{data.get('page', '3')}</span> <span>{_e(data.get('page', '3'))}</span>
</div> </div>
</section> </section>
''' '''
@ -514,21 +531,21 @@ def generate_metrics_slide(data):
metrics_html = ''.join([f''' metrics_html = ''.join([f'''
<div class="card metric"> <div class="card metric">
<div class="metric-value">{m['value']}</div> <div class="metric-value">{_e(m.get('value', ''))}</div>
<div class="metric-label">{m['label']}</div> <div class="metric-label">{_e(m.get('label', ''))}</div>
</div> </div>
''' for m in metrics[:4]]) ''' for m in metrics[:4]])
return f''' return f'''
<section class="slide slide--surface slide--glow"> <section class="slide slide--surface slide--glow">
<div class="badge mb-6">Traction</div> <div class="badge mb-6">Traction</div>
<h2 class="slide-heading mb-8 text-center">{data.get('headline', 'Our Growth')}</h2> <h2 class="slide-heading mb-8 text-center">{_e(data.get('headline', 'Our Growth'))}</h2>
<div class="grid grid-4 gap-6" style="flex: 1; align-items: center;"> <div class="grid grid-4 gap-6" style="flex: 1; align-items: center;">
{metrics_html} {metrics_html}
</div> </div>
<div class="slide-footer"> <div class="slide-footer">
<span>{data.get('company', 'Company Name')}</span> <span>{_e(data.get('company', 'Company Name'))}</span>
<span>{data.get('page', '4')}</span> <span>{_e(data.get('page', '4'))}</span>
</div> </div>
</section> </section>
''' '''
@ -544,25 +561,25 @@ def generate_chart_slide(data):
]) ])
bars_html = ''.join([f''' bars_html = ''.join([f'''
<div class="bar" style="height: {b['value']}%;"> <div class="bar" style="height: {int(b.get('value', 0))}%;">
<span class="bar-value">{b.get('display', str(b['value']) + '%')}</span> <span class="bar-value">{_e(b.get('display', str(b.get('value', 0)) + '%'))}</span>
<span class="bar-label">{b['label']}</span> <span class="bar-label">{_e(b.get('label', ''))}</span>
</div> </div>
''' for b in bars]) ''' for b in bars])
return f''' return f'''
<section class="slide"> <section class="slide">
<div class="badge mb-6">{data.get('badge', 'Growth')}</div> <div class="badge mb-6">{_e(data.get('badge', 'Growth'))}</div>
<h2 class="slide-heading mb-8">{data.get('headline', 'Revenue Growth')}</h2> <h2 class="slide-heading mb-8">{_e(data.get('headline', 'Revenue Growth'))}</h2>
<div class="chart-container" style="flex: 1;"> <div class="chart-container" style="flex: 1;">
<div class="chart-title">{data.get('chart_title', 'Quarterly Revenue')}</div> <div class="chart-title">{_e(data.get('chart_title', 'Quarterly Revenue'))}</div>
<div class="bar-chart" style="flex: 1; padding-bottom: 40px;"> <div class="bar-chart" style="flex: 1; padding-bottom: 40px;">
{bars_html} {bars_html}
</div> </div>
</div> </div>
<div class="slide-footer"> <div class="slide-footer">
<span>{data.get('company', 'Company Name')}</span> <span>{_e(data.get('company', 'Company Name'))}</span>
<span>{data.get('page', '5')}</span> <span>{_e(data.get('page', '5'))}</span>
</div> </div>
</section> </section>
''' '''
@ -574,13 +591,13 @@ def generate_testimonial_slide(data):
<section class="slide slide--surface flex flex-col justify-center"> <section class="slide slide--surface flex flex-col justify-center">
<div class="badge mb-6">What They Say</div> <div class="badge mb-6">What They Say</div>
<div class="testimonial" style="max-width: 900px;"> <div class="testimonial" style="max-width: 900px;">
<p class="testimonial-quote">"{data.get('quote', 'This product changed how we work. Incredible results.')}"</p> <p class="testimonial-quote">"{_e(data.get('quote', 'This product changed how we work. Incredible results.'))}"</p>
<p class="testimonial-author">{data.get('author', 'Jane Doe')}</p> <p class="testimonial-author">{_e(data.get('author', 'Jane Doe'))}</p>
<p class="testimonial-role">{data.get('role', 'CEO, Example Company')}</p> <p class="testimonial-role">{_e(data.get('role', 'CEO, Example Company'))}</p>
</div> </div>
<div class="slide-footer"> <div class="slide-footer">
<span>{data.get('company', 'Company Name')}</span> <span>{_e(data.get('company', 'Company Name'))}</span>
<span>{data.get('page', '6')}</span> <span>{_e(data.get('page', '6'))}</span>
</div> </div>
</section> </section>
''' '''
@ -590,14 +607,14 @@ def generate_cta_slide(data):
"""Closing CTA slide""" """Closing CTA slide"""
return f''' return f'''
<section class="slide slide--gradient flex flex-col items-center justify-center text-center"> <section class="slide slide--gradient flex flex-col items-center justify-center text-center">
<h2 class="slide-heading mb-6" style="color: var(--color-foreground);">{data.get('headline', 'Ready to get started?')}</h2> <h2 class="slide-heading mb-6" style="color: var(--color-foreground);">{_e(data.get('headline', 'Ready to get started?'))}</h2>
<p class="slide-body mb-8" style="color: rgba(255,255,255,0.8);">{data.get('subheadline', 'Join thousands of teams already using our solution.')}</p> <p class="slide-body mb-8" style="color: rgba(255,255,255,0.8);">{_e(data.get('subheadline', 'Join thousands of teams already using our solution.'))}</p>
<div class="flex gap-4"> <div class="flex gap-4">
<a href="{data.get('cta_url', '#')}" class="btn" style="background: var(--color-foreground); color: var(--color-primary);">{data.get('cta', 'Start Free Trial')}</a> <a href="{_safe_url(data.get('cta_url', '#'))}" class="btn" style="background: var(--color-foreground); color: var(--color-primary);">{_e(data.get('cta', 'Start Free Trial'))}</a>
</div> </div>
<div class="slide-footer" style="border-color: rgba(255,255,255,0.2); color: rgba(255,255,255,0.6);"> <div class="slide-footer" style="border-color: rgba(255,255,255,0.2); color: rgba(255,255,255,0.6);">
<span>{data.get('contact', 'contact@example.com')}</span> <span>{_e(data.get('contact', 'contact@example.com'))}</span>
<span>{data.get('website', 'www.example.com')}</span> <span>{_e(data.get('website', 'www.example.com'))}</span>
</div> </div>
</section> </section>
''' '''
@ -632,7 +649,7 @@ def generate_deck(slides_data, title="Pitch Deck"):
tokens_rel_path = "../../../assets/design-tokens.css" tokens_rel_path = "../../../assets/design-tokens.css"
return SLIDE_TEMPLATE.format( return SLIDE_TEMPLATE.format(
title=title, title=escape(str(title)),
tokens_css_path=tokens_rel_path, tokens_css_path=tokens_rel_path,
slides_content=slides_html slides_content=slides_html
) )