diff --git a/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs b/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs
index 86c19e88..e7bc1711 100644
--- a/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs
+++ b/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs
@@ -11,7 +11,7 @@
const fs = require('fs');
const path = require('path');
-const { execSync } = require('child_process');
+const { execFileSync } = require('child_process');
// Paths
const BRAND_GUIDELINES = 'docs/brand-guidelines.md';
@@ -250,7 +250,7 @@ function main() {
const generateScript = path.resolve(process.cwd(), GENERATE_TOKENS_SCRIPT);
if (fs.existsSync(generateScript)) {
try {
- execSync(`node ${generateScript} --config ${DESIGN_TOKENS_JSON} -o ${DESIGN_TOKENS_CSS}`, {
+ execFileSync('node', [generateScript, '--config', DESIGN_TOKENS_JSON, '-o', DESIGN_TOKENS_CSS], {
cwd: process.cwd(),
stdio: 'inherit'
});
diff --git a/.claude/skills/design-system/scripts/generate-slide.py b/.claude/skills/design-system/scripts/generate-slide.py
index 228a50a3..a04e5111 100644
--- a/.claude/skills/design-system/scripts/generate-slide.py
+++ b/.claude/skills/design-system/scripts/generate-slide.py
@@ -7,10 +7,16 @@
"""
import argparse
+import html as html_mod
import json
from pathlib import Path
from datetime import datetime
+
+def esc(value):
+ """Escape user-provided values for safe HTML embedding."""
+ return html_mod.escape(str(value))
+
# Paths
SCRIPT_DIR = Path(__file__).parent
DATA_DIR = SCRIPT_DIR.parent / "data"
@@ -412,16 +418,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')}
+ {esc(data.get('badge', 'Pitch Deck'))}
+ {esc(data.get('title', 'Your Title Here'))}
+ {esc(data.get('subtitle', 'Your compelling subtitle'))}
'''
@@ -432,27 +438,27 @@ def generate_problem_slide(data):
return f'''
The Problem
- {data.get('headline', 'The problem your audience faces')}
+ {esc(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')}
+
{esc(data.get('pain_1_title', 'Pain Point 1'))}
+
{esc(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')}
+
{esc(data.get('pain_2_title', 'Pain Point 2'))}
+
{esc(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')}
+
{esc(data.get('pain_3_title', 'Pain Point 3'))}
+
{esc(data.get('pain_3_desc', 'Description of the third pain point'))}
'''
@@ -463,28 +469,28 @@ def generate_solution_slide(data):
return f'''
The Solution
- {data.get('headline', 'How we solve this')}
+ {esc(data.get('headline', 'How we solve this'))}
✓
-
{data.get('feature_1_title', 'Feature 1')}
-
{data.get('feature_1_desc', 'Description of feature 1')}
+
{esc(data.get('feature_1_title', 'Feature 1'))}
+
{esc(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')}
+
{esc(data.get('feature_2_title', 'Feature 2'))}
+
{esc(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')}
+
{esc(data.get('feature_3_title', 'Feature 3'))}
+
{esc(data.get('feature_3_desc', 'Description of feature 3'))}
@@ -496,8 +502,8 @@ def generate_solution_slide(data):
'''
@@ -514,21 +520,21 @@ def generate_metrics_slide(data):
metrics_html = ''.join([f'''
-
{m['value']}
-
{m['label']}
+
{esc(m['value'])}
+
{esc(m['label'])}
''' for m in metrics[:4]])
return f'''
Traction
- {data.get('headline', 'Our Growth')}
+ {esc(data.get('headline', 'Our Growth'))}
{metrics_html}
'''
@@ -544,25 +550,25 @@ def generate_chart_slide(data):
])
bars_html = ''.join([f'''
-
-
{b.get('display', str(b['value']) + '%')}
-
{b['label']}
+
+ {esc(b.get('display', str(b['value']) + '%'))}
+ {esc(b['label'])}
''' for b in bars])
return f'''
- {data.get('badge', 'Growth')}
- {data.get('headline', 'Revenue Growth')}
+ {esc(data.get('badge', 'Growth'))}
+ {esc(data.get('headline', 'Revenue Growth'))}
-
{data.get('chart_title', 'Quarterly Revenue')}
+
{esc(data.get('chart_title', 'Quarterly Revenue'))}
{bars_html}
'''
@@ -574,30 +580,39 @@ 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')}
+
"{esc(data.get('quote', 'This product changed how we work. Incredible results.'))}"
+
{esc(data.get('author', 'Jane Doe'))}
+
{esc(data.get('role', 'CEO, Example Company'))}
'''
+def _sanitize_url(url):
+ """Ensure URL is safe for href attribute (no javascript: or data: URIs)."""
+ url = str(url).strip()
+ if url.startswith(('http://', 'https://', '/', '#', 'mailto:')):
+ return html_mod.escape(url, quote=True)
+ return '#'
+
+
def generate_cta_slide(data):
"""Closing CTA slide"""
+ cta_url = _sanitize_url(data.get('cta_url', '#'))
return f'''
- {data.get('headline', 'Ready to get started?')}
- {data.get('subheadline', 'Join thousands of teams already using our solution.')}
+ {esc(data.get('headline', 'Ready to get started?'))}
+ {esc(data.get('subheadline', 'Join thousands of teams already using our solution.'))}
'''
diff --git a/.claude/skills/design/scripts/cip/render-html.py b/.claude/skills/design/scripts/cip/render-html.py
index 34e37826..1c2c4545 100644
--- a/.claude/skills/design/scripts/cip/render-html.py
+++ b/.claude/skills/design/scripts/cip/render-html.py
@@ -8,6 +8,7 @@
"""
import argparse
+import html
import json
import os
import sys
@@ -140,13 +141,19 @@ def generate_html(brand_name, industry, images_dir, output_path=None, style=None
style_info = brief.get("style", {})
industry_info = brief.get("industry", {})
+ # Sanitize user-provided values for safe HTML embedding
+ safe_brand = html.escape(brand_name)
+ safe_industry = html.escape(industry_info.get("Industry", industry.title()))
+ safe_style = html.escape(style_info.get("Style Name", "Corporate"))
+ safe_mood = html.escape(style_info.get("Mood", "Professional"))
+
# Build HTML
html_parts = [f'''
-
{brand_name} - Corporate Identity Program
+
{safe_brand} - Corporate Identity Program