This document provides a comprehensive overview of Sparktype's security model, focusing on JavaScript execution, theme validation, and external resource integration.
- Security Philosophy
- SiteViewer Sandbox Architecture
- Theme Validation System
- External Script Integration
- Attack Vectors & Mitigations
- Developer Guidelines
Sparktype follows a defense-in-depth security model with these core principles:
- Preview Isolation: No untrusted code executes in the editor/preview environment
- Validation at Import: All themes are validated before acceptance
- Allowlist-Based: Only explicitly trusted resources are permitted
- User Control: Clear visibility into what external resources themes use
- Export Freedom: Published sites are user-controlled and unrestricted
┌─────────────────────────────────────────────┐
│ SPARKTYPE EDITOR │
│ (Fully Controlled Environment) │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ SiteViewer (Sandboxed iFrame) │ │
│ │ - AlpineJS (Sparktype-controlled) │ │
│ │ - Handlebars templates (validated) │ │
│ │ - No external scripts │ │
│ │ - No custom JavaScript │ │
│ └───────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
↓
[User Exports]
↓
┌─────────────────────────────────────────────┐
│ PUBLISHED SITE │
│ (User-Controlled Environment) │
│ │
│ - All theme templates │
│ - External scripts (if declared) │
│ - User-added scripts │
│ - User responsibility │
└─────────────────────────────────────────────┘
File: /Users/mattkevan/Sites/sparktype/src/features/viewer/components/SiteViewer.tsx
The SiteViewer component renders site previews in a sandboxed iframe with strict security controls.
const sandboxAttributes =
process.env.NODE_ENV === 'development'
? 'allow-scripts allow-forms allow-same-origin'
: 'allow-scripts allow-forms';Development Mode:
allow-scripts: Permits JavaScript execution (for AlpineJS + navigation)allow-forms: Allows form submissionallow-same-origin: Enables LocalStorage access (needed for development)
Production Mode:
allow-scripts: Permits JavaScript executionallow-forms: Allows form submission- ❌ No
allow-same-origin: Prevents access to parent window storage/cookies
Even with allow-scripts, the iframe sandbox prevents:
- ❌ Accessing
window.toporwindow.parent - ❌ Opening popups (
window.open) - ❌ Downloading files
- ❌ Accessing localStorage (in production)
- ❌ Setting cookies on parent domain
- ❌ Breaking out of the iframe
Key Insight: The communication script (lines 81-163 in SiteViewer.tsx) is injected by Sparktype, not by themes.
const communicationScript = `
<script>
// Navigation handling code
// Injected by Sparktype, NOT by theme
</script>
`;
const finalHtml = pureHtml.replace('</body>', `${communicationScript}</body>`);What this means:
- Themes cannot inject arbitrary
<script>tags - All JavaScript in preview is Sparktype-controlled
- Theme templates are pure Handlebars (no script execution capability)
AlpineJS is loaded by Sparktype, not by themes:
File: /Users/mattkevan/Sites/sparktype/public/themes/sparksite/base.hbs
<body>
<div x-data="{ isMobileMenuOpen: false }">
<!-- Theme content with Alpine directives -->
</div>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
</body>Security properties:
- AlpineJS version controlled by Sparktype
- Declarative directives only (
x-data,x-show,x-on:) - No
eval()orFunction()constructors exposed to themes - Directives are data attributes, not executable code
┌─────────────┐
│ Theme ZIP │
└──────┬──────┘
│
↓
┌─────────────────────────────────┐
│ 1. Extract & Size Check │
│ - File count limit (500) │
│ - Total size limit (20MB) │
│ - Decompression ratio (100:1)│
└──────┬──────────────────────────┘
│
↓
┌─────────────────────────────────┐
│ 2. File Type Validation │
│ - Whitelist check │
│ - Block .js, .exe, etc. │
│ - Path traversal check │
└──────┬──────────────────────────┘
│
↓
┌─────────────────────────────────┐
│ 3. Manifest Validation │
│ - Schema compliance │
│ - Required files exist │
│ - Layout references valid │
└──────┬──────────────────────────┘
│
↓
┌─────────────────────────────────┐
│ 4. Template Validation │
│ - Handlebars compilation │
│ - No <script> tags │
│ - No inline event handlers │
│ - No javascript: URLs │
└──────┬──────────────────────────┘
│
↓
┌─────────────────────────────────┐
│ 5. CSS Sanitization │
│ - @import domain allowlist │
│ - No javascript: in url() │
│ - Font CDN validation │
└──────┬──────────────────────────┘
│
↓
┌─────────────┐
│ ✅ ACCEPTED │
└─────────────┘
Configuration: src/config/editorConfig.ts → SECURITY_CONFIG.THEME_ALLOWED_EXTENSIONS
Allowed:
.hbs,.json,.css- Templates & configuration.woff,.woff2,.ttf,.otf,.eot- Fonts.svg,.png,.jpg,.jpeg,.gif,.webp- Images.md,.txt- Documentation
Blocked:
.js,.mjs,.ts,.jsx,.tsx- JavaScript.exe,.sh,.bat,.cmd,.app- Executables.php,.py,.rb,.pl,.asp,.jsp- Server scripts
Service: src/core/services/themeValidation.service.ts
function validateHandlebarsTemplate(content: string): ValidationResult {
const errors: string[] = [];
// 1. Block <script> tags
if (/<script[\s>]/i.test(content)) {
errors.push('Script tags not allowed in templates');
}
// 2. Block inline event handlers
if (/\son(click|error|load|mouse\w+|key\w+)\s*=/i.test(content)) {
errors.push('Inline event handlers not allowed');
}
// 3. Block javascript: URLs
if (/(?:href|src)\s*=\s*["']?\s*javascript:/i.test(content)) {
errors.push('javascript: URLs not allowed');
}
// 4. Verify template compiles
try {
Handlebars.compile(content);
} catch (error) {
errors.push(`Template syntax error: ${error.message}`);
}
return { valid: errors.length === 0, errors };
}What's allowed:
- ✅ AlpineJS directives (
x-data,x-show,x-on:click) - ✅ Handlebars expressions (
{{title}},{{#if}},{{#each}}) - ✅ Handlebars helpers (all are safe, no code execution)
What's blocked:
- ❌
<script>tags - ❌
onclick="",onerror="", etc. - ❌
href="javascript:alert()" - ❌ Invalid Handlebars syntax
Font Domain Allowlist: SECURITY_CONFIG.TRUSTED_FONT_DOMAINS
Supported font services:
- Google Fonts (
fonts.googleapis.com,fonts.gstatic.com) - Bunny Fonts (
fonts.bunny.net) - Privacy-friendly alternative - Adobe Fonts / Typekit (
use.typekit.net,use.typekit.com) - Fonts.com (
fast.fonts.net,fonts.com) - Font Awesome (
use.fontawesome.com) - Typography.com (
cloud.typography.com)
function validateCSS(content: string): ValidationResult {
const errors: string[] = [];
// Check @import statements
const importRegex = /@import\s+(?:url\()?['"]?([^'")\s]+)['"]?\)?/gi;
for (const match of [...content.matchAll(importRegex)]) {
const url = match[1];
const hostname = new URL(url).hostname;
// Only allow trusted font domains
if (!TRUSTED_FONT_DOMAINS.includes(hostname)) {
errors.push(`Unauthorized @import: ${hostname}`);
}
}
// Block javascript: in CSS
if (/url\s*\(\s*['"]?javascript:/i.test(content)) {
errors.push('JavaScript URLs in CSS not allowed');
}
return { valid: errors.length === 0, errors };
}Sanitization removes unauthorized imports:
function sanitizeCSS(content: string): string {
return content.replace(
/@import\s+(?:url\()?['"]?([^'")\s]+)['"]?\)?;?/gi,
(match, url) => {
const hostname = new URL(url).hostname;
return TRUSTED_FONT_DOMAINS.includes(hostname) ? match : '';
}
);
}Scope: Published sites only (never in preview)
Themes will be able to declare external scripts in their manifest for services like:
- E-commerce (Snipcart, Stripe)
- Analytics (Plausible, Fathom)
- Forms (hCaptcha, reCAPTCHA)
Example (theme.json):
{
"externalScripts": [
{
"id": "snipcart",
"name": "Snipcart Shopping Cart",
"src": "https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.js",
"integrity": "sha384-...",
"crossorigin": "anonymous",
"defer": true,
"attributes": {
"id": "snipcart",
"data-api-key": "{user_provided}"
},
"required": true,
"category": "ecommerce"
}
]
}Security controls:
- Domain allowlist: Only
TRUSTED_SCRIPT_DOMAINSaccepted - SRI encouraged: Subresource Integrity hashes validated
- Export only: Scripts injected only when
isExport: true - User consent: Clear UI showing what scripts will load
Attack: Malicious theme injects <script> tags or event handlers
Mitigation:
- ✅ Template validation blocks all
<script>tags - ✅ Inline event handlers blocked
- ✅ Handlebars auto-escapes output by default
- ✅ javascript: URLs blocked in href/src
Status: ✅ PROTECTED
Attack: Malicious CSS exfiltrates data or performs clickjacking
Mitigation:
- ✅ @import limited to trusted font domains
- ✅ javascript: URLs in CSS blocked
- ✅ CSS sanitization removes unauthorized imports
⚠️ CSS can still be used for visual attacks (low risk)
Status: ✅ PROTECTED (with minor limitations)
Attack: Highly compressed ZIP expands to exhaust memory
Mitigation:
- ✅ Decompression ratio limit (100:1)
- ✅ Total size limit (20MB)
- ✅ File count limit (500 files)
- ✅ Per-file size limit (5MB)
Status: ✅ PROTECTED
Attack: ZIP contains ../../ paths to escape theme directory
Mitigation:
- ✅ Path validation rejects
..in file paths - ✅ Paths must be relative (no leading
/) - ✅ Special characters blocked in paths
Status: ✅ PROTECTED
Attack: Theme tricks Sparktype into running malicious JS in preview
Mitigation:
- ✅ SiteViewer iframe sandbox
- ✅ No
<script>tags allowed in templates - ✅ No inline event handlers
- ✅ Navigation script injected by Sparktype, not theme
- ✅ AlpineJS loaded by Sparktype (controlled version)
Status: ✅ PROTECTED
Attack: Theme loads malicious scripts from compromised CDN
Mitigation:
- ✅ Scripts never load in preview (export only)
- ✅ Domain allowlist enforced
- ✅ SRI (Subresource Integrity) validation
- ✅ User must explicitly configure API keys
- ✅ Clear warnings in UI
Status: ✅ PROTECTED (with user awareness)
- Use AlpineJS directives for interactivity
- Import fonts from trusted CDNs (Google Fonts, Bunny Fonts, Typekit)
- Include self-hosted fonts in theme package
- Use Handlebars helpers and partials
- Declare external scripts in manifest (for published sites)
- Document required API keys clearly
- Include
.jsfiles in theme package - Add
<script>tags to templates - Use inline event handlers (
onclick="") - Use
javascript:URLs - Import CSS from untrusted domains
- Rely on scripts in preview (they won't load)
When modifying theme or rendering code:
- ✅ Does this allow arbitrary JavaScript execution?
- ✅ Does this bypass the sandbox?
- ✅ Does this trust user-provided data without validation?
- ✅ Does this load external resources in preview?
- ✅ Does this expand the attack surface?
# Test theme validation
npm run test -- themeValidation.service.test.ts
# Test with malicious theme samples
# (Create test fixtures with XSS attempts, path traversal, etc.)To add a new trusted domain:
- Research the service thoroughly
- Verify it's a legitimate CDN/service provider
- Check security practices (HTTPS, SRI support)
- Add to
SECURITY_CONFIGineditorConfig.ts - Document in
docs/THEME_DEVELOPMENT.md - Update validation tests
| Date | Change | Rationale |
|---|---|---|
| 2025-01 | Initial security architecture | Establish baseline protections |
| 2025-01 | Added font domain allowlist | Support professional font services while maintaining security |
| 2025-01 | Implemented theme validation service | Prevent malicious themes at import time |
If you discover a security vulnerability in Sparktype:
- DO NOT create a public GitHub issue
- Email security concerns to: [security@sparktype.org]
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will respond within 48 hours and work with you to address the issue.