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'))}

- {data.get('cta', 'Get Started')} - {data.get('secondary_cta', 'Learn More')} + {esc(data.get('cta', 'Get Started'))} + {esc(data.get('secondary_cta', 'Learn More'))}
''' @@ -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