Skip to main content

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

  1. Always start with report-only mode to identify issues
  2. Test thoroughly with actual Webflow content
  3. Consider different CSP policies for different project types
  4. Monitor CSP violations regularly
  5. Update trusted domains based on enabled scripts
  6. Use nonces for inline scripts when possible
  7. Document any CSP relaxations and their reasons