Content Security Policy (CSP) Implementation Guide
Overview
This guide provides a comprehensive implementation strategy for adding Content Security Policy headers to the WES application, which processes Webflow pages and deploys them to various hosting platforms.
Implementation Layers
1. Production Host/CDN Level (Primary)
S3 + CloudFront Configuration
// CloudFront Response Headers Policy
{
"Content-Security-Policy": "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://ajax.googleapis.com 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com;"
}
Nginx Configuration (Custom Hosting)
# Nginx configuration
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline';" always;
2. HTML Meta Tag Implementation (Fallback Method)
Add to app/Listeners/HtmlAssetCreated/SubstituteHtmlPaths.php:
private function addCSPMetaTag($source, $project) {
$allowedDomains = $this->getAllowedScriptDomains($project);
$csp = sprintf(
"default-src 'self'; script-src 'self' %s; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;",
implode(' ', $allowedDomains)
);
$metaTag = sprintf('<meta http-equiv="Content-Security-Policy" content="%s">', $csp);
// Insert after <head> tag
$source = str_replace('<head>', '<head>' . PHP_EOL . $metaTag, $source);
return $source;
}
3. Dynamic CSP Generation Based on Enabled Scripts
// In SubstituteHtmlPaths.php
private function generateDynamicCSP($project, $asset) {
$cspDirectives = [
'default-src' => ["'self'"],
'script-src' => ["'self'"],
'style-src' => ["'self'", "'unsafe-inline'"],
'img-src' => ["'self'", 'data:', 'https:'],
'font-src' => ["'self'", 'data:']
];
// Get enabled scripts for this asset
$enabledScripts = $asset->scripts()
->where('enabled', 1)
->get();
foreach ($enabledScripts as $script) {
if ($script->source) {
$domain = parse_url($script->source, PHP_URL_HOST);
if ($domain && !in_array($domain, $cspDirectives['script-src'])) {
$cspDirectives['script-src'][] = 'https://' . $domain;
}
}
// If inline scripts exist and are enabled
if ($script->content && !in_array("'unsafe-inline'", $cspDirectives['script-src'])) {
$cspDirectives['script-src'][] = "'unsafe-inline'";
}
}
// Handle Webflow-specific requirements
if ($this->hasWebflowScripts($project)) {
$cspDirectives['script-src'][] = 'https://ajax.googleapis.com';
$cspDirectives['script-src'][] = 'https://d3e54v103j8qbb.cloudfront.net';
$cspDirectives['connect-src'] = ["'self'", 'https://cdn.jsdelivr.net'];
}
// Build CSP string
$cspString = '';
foreach ($cspDirectives as $directive => $sources) {
$cspString .= $directive . ' ' . implode(' ', $sources) . '; ';
}
return trim($cspString);
}
Database Schema Updates
Migration for CSP Configuration
// New migration file
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddCspConfigToProjectMeta extends Migration
{
public function up()
{
// Store CSP config in project metadata
// Using existing wes_project_meta table
// Config stored as JSON in 'value' column with key 'csp_config'
}
}
// Usage in code
ProjectMetaRepository::save($project->id, 'csp_config', [
'enabled' => true,
'mode' => 'report-only', // or 'enforce'
'custom_domains' => ['analytics.google.com', 'cdn.segment.com'],
'allow_unsafe_inline' => false,
'report_uri' => '/api/csp-report'
]);
Integration Points in Existing Code
Update SubstituteHtmlPaths.php
Add after line 424 (after removing integrity attributes):
// Add CSP header as meta tag
$cspConfig = ProjectMetaRepository::getValue($project->id, 'csp_config');
if ($cspConfig && $cspConfig->enabled) {
$csp = $this->generateDynamicCSP($project, $asset);
// Add reporting endpoint if in report-only mode
if ($cspConfig->mode === 'report-only') {
$csp .= " report-uri " . env('CSP_REPORT_URI', '/api/csp-report');
$metaTag = '<meta http-equiv="Content-Security-Policy-Report-Only" content="' . $csp . '">';
} else {
$metaTag = '<meta http-equiv="Content-Security-Policy" content="' . $csp . '">';
}
$source = str_replace('</head>', $metaTag . PHP_EOL . '</head>', $source);
// Store CSP for reference
$asset->csp_header = $csp;
$asset->save();
}
React UI Components
CSPSettings.tsx Component
import React, { useState, useEffect, useContext } from 'react';
import { Switch, Select, Input, Tag, message } from 'antd';
import { AppStateContext, AppDispatchContext } from '@/components/contexts/AppContext';
import ProjectClient from '@/clients/ProjectClient';
const { Option } = Select;
interface CSPConfig {
enabled: boolean;
mode: 'enforce' | 'report-only';
custom_domains: string[];
allow_unsafe_inline: boolean;
report_uri?: string;
}
const CSPSettings: React.FC = () => {
const { state } = useContext(AppStateContext);
const { dispatch } = useContext(AppDispatchContext);
const projectClient = new ProjectClient();
const [cspConfig, setCSPConfig] = useState<CSPConfig>({
enabled: false,
mode: 'report-only',
custom_domains: [],
allow_unsafe_inline: false
});
useEffect(() => {
if (state.project?.data?.csp_config) {
setCSPConfig(state.project.data.csp_config);
}
}, [state.project]);
const updateCSPConfig = async () => {
try {
const updatedProject = await projectClient.updateProject(
state.project!.id!,
{ ...state.project!, data: { csp_config: cspConfig } }
);
dispatch({ type: 'UPDATE_PROJECT', payload: updatedProject });
message.success('CSP configuration updated');
} catch (error) {
message.error('Failed to update CSP configuration');
}
};
const addDomain = (domain: string) => {
if (domain && !cspConfig.custom_domains.includes(domain)) {
setCSPConfig({
...cspConfig,
custom_domains: [...cspConfig.custom_domains, domain]
});
}
};
const removeDomain = (domain: string) => {
setCSPConfig({
...cspConfig,
custom_domains: cspConfig.custom_domains.filter(d => d !== domain)
});
};
return (
<div className="csp-settings">
<h3>Content Security Policy Settings</h3>
<div className="settings-item">
<label>Enable CSP</label>
<Switch
checked={cspConfig.enabled}
onChange={(checked) => setCSPConfig({...cspConfig, enabled: checked})}
/>
</div>
{cspConfig.enabled && (
<>
<div className="settings-item">
<label>Mode</label>
<Select
value={cspConfig.mode}
onChange={(mode) => setCSPConfig({...cspConfig, mode})}
style={{ width: 200 }}
>
<Option value="report-only">Report Only (Test)</Option>
<Option value="enforce">Enforce (Production)</Option>
</Select>
</div>
<div className="settings-item">
<label>Allow Unsafe Inline Scripts</label>
<Switch
checked={cspConfig.allow_unsafe_inline}
onChange={(checked) => setCSPConfig({...cspConfig, allow_unsafe_inline: checked})}
/>
</div>
<div className="settings-item">
<label>Trusted Domains</label>
<div className="domain-list">
{cspConfig.custom_domains.map(domain => (
<Tag
key={domain}
closable
onClose={() => removeDomain(domain)}
>
{domain}
</Tag>
))}
</div>
<Input.Search
placeholder="Add domain (e.g., analytics.google.com)"
onSearch={addDomain}
style={{ marginTop: 8 }}
/>
</div>
<div className="settings-item">
<label>Report URI (Optional)</label>
<Input
value={cspConfig.report_uri}
onChange={(e) => setCSPConfig({...cspConfig, report_uri: e.target.value})}
placeholder="/api/csp-report"
/>
</div>
</>
)}
<button onClick={updateCSPConfig} className="btn-primary">
Save CSP Configuration
</button>
</div>
);
};
export default CSPSettings;
CSP Violation Reporting Endpoint
API Route (routes/api.php)
Route::post('/csp-report', [CSPReportController::class, 'report']);
Controller (app/Http/Controllers/CSPReportController.php)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Models\Project\ProjectLog;
class CSPReportController extends Controller
{
public function report(Request $request)
{
$report = json_decode($request->getContent(), true);
if (isset($report['csp-report'])) {
$violation = $report['csp-report'];
// Log CSP violation
Log::warning('CSP Violation', [
'document-uri' => $violation['document-uri'] ?? '',
'blocked-uri' => $violation['blocked-uri'] ?? '',
'violated-directive' => $violation['violated-directive'] ?? '',
'source-file' => $violation['source-file'] ?? '',
'line-number' => $violation['line-number'] ?? '',
]);
// Optional: Store in database for analysis
// CSPViolation::create([...]);
}
return response()->json(['status' => 'received'], 204);
}
}
Implementation Checklist
- Add CSP configuration to project metadata structure
- Implement
generateDynamicCSP()method in SubstituteHtmlPaths.php - Add
addCSPMetaTag()method in SubstituteHtmlPaths.php - Create CSPSettings React component
- Add CSP settings to Settings page
- Create CSP violation reporting endpoint
- Test in report-only mode first
- Document trusted domains for Webflow sites
- Add CSP configuration to project setup wizard
- Implement CSP violation monitoring dashboard
Security Levels
Level 1: Basic (Development)
default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;
Level 2: Moderate (Staging)
default-src 'self';
script-src 'self' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline';
Level 3: Strict (Production)
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'nonce-{random}';
object-src 'none';
base-uri 'self';
Common Webflow Domains to Whitelist
const webflowDomains = [
'https://ajax.googleapis.com',
'https://d3e54v103j8qbb.cloudfront.net',
'https://cdn.jsdelivr.net',
'https://uploads-ssl.webflow.com',
'https://assets.website-files.com',
'https://fonts.googleapis.com',
'https://fonts.gstatic.com'
];
Notes
- Always start with report-only mode to identify issues
- Test thoroughly with actual Webflow content
- Consider different CSP policies for different project types
- Monitor CSP violations regularly
- Update trusted domains based on enabled scripts
- Use nonces for inline scripts when possible
- Document any CSP relaxations and their reasons