Setting Up CSP for WordPress
Setting Up CSP for WordPress
Begin implementing CSP on WordPress by creating a flexible system that can handle the platform's dynamic nature. Using a plugin-based approach provides the most maintainable solution.
Creating a Custom CSP Plugin:
<?php
/**
* Plugin Name: WordPress CSP Manager
* Description: Comprehensive Content Security Policy implementation for WordPress
* Version: 1.0.0
* Author: Your Name
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_CSP_Manager {
private $nonce;
private $report_only = true;
private $directives = [];
public function __construct() {
add_action('init', [$this, 'init']);
add_action('send_headers', [$this, 'send_csp_headers']);
add_action('wp_enqueue_scripts', [$this, 'prepare_frontend_csp']);
add_action('admin_enqueue_scripts', [$this, 'prepare_admin_csp']);
add_action('wp_ajax_csp_report', [$this, 'handle_csp_report']);
add_action('wp_ajax_nopriv_csp_report', [$this, 'handle_csp_report']);
// Admin menu
add_action('admin_menu', [$this, 'add_admin_menu']);
}
public function init() {
// Generate nonce for this request
$this->nonce = wp_create_nonce('csp_nonce');
// Set report-only mode based on environment
$this->report_only = (defined('WP_ENV') && WP_ENV === 'production') ? false : true;
// Initialize base directives
$this->init_directives();
}
private function init_directives() {
$site_url = parse_url(home_url(), PHP_URL_HOST);
$this->directives = [
'default-src' => ["'self'"],
'script-src' => ["'self'", "'nonce-" . $this->nonce . "'"],
'style-src' => ["'self'", "'nonce-" . $this->nonce . "'"],
'img-src' => ["'self'", 'data:', 'https:'],
'font-src' => ["'self'"],
'connect-src' => ["'self'"],
'frame-ancestors' => ["'self'"],
'base-uri' => ["'self'"],
'form-action' => ["'self'"],
'report-uri' => ['/wp-admin/admin-ajax.php?action=csp_report']
];
// Add WordPress-specific sources
$this->add_wordpress_core_sources();
}
private function add_wordpress_core_sources() {
// WordPress core requirements
$this->directives['script-src'][] = "'unsafe-inline'"; // Unfortunately needed for WordPress
$this->directives['style-src'][] = "'unsafe-inline'";
// Common WordPress CDNs
$this->directives['script-src'][] = 'https://s0.wp.com';
$this->directives['style-src'][] = 'https://fonts.googleapis.com';
$this->directives['font-src'][] = 'https://fonts.gstatic.com';
// Gravatar
$this->directives['img-src'][] = 'https://secure.gravatar.com';
$this->directives['img-src'][] = 'https://s.w.org';
}
public function prepare_frontend_csp() {
// Detect and add plugin-specific sources
$this->detect_plugin_resources();
// Add theme-specific sources
$this->add_theme_sources();
// Make nonce available to scripts
wp_localize_script('jquery', 'wpCSP', [
'nonce' => $this->nonce
]);
}
public function prepare_admin_csp() {
// Relax CSP for admin area
$this->directives['script-src'][] = "'unsafe-eval'"; // Required for some admin features
$this->directives['frame-src'] = ["'self'", 'https://www.youtube.com', 'https://player.vimeo.com'];
// TinyMCE requirements
$this->directives['img-src'][] = 'blob:';
$this->directives['media-src'] = ["'self'", 'blob:'];
}
private function detect_plugin_resources() {
// Detect active plugins and their requirements
$active_plugins = get_option('active_plugins');
foreach ($active_plugins as $plugin) {
$plugin_name = dirname($plugin);
// Common plugin CSP requirements
switch ($plugin_name) {
case 'contact-form-7':
$this->directives['script-src'][] = "'unsafe-eval'"; // For reCAPTCHA
$this->directives['frame-src'][] = 'https://www.google.com';
break;
case 'woocommerce':
$this->directives['script-src'][] = 'https://checkout.stripe.com';
$this->directives['frame-src'][] = 'https://checkout.stripe.com';
$this->directives['connect-src'][] = 'https://api.stripe.com';
break;
case 'google-analytics-for-wordpress':
$this->directives['script-src'][] = 'https://www.google-analytics.com';
$this->directives['img-src'][] = 'https://www.google-analytics.com';
$this->directives['connect-src'][] = 'https://www.google-analytics.com';
break;
}
}
}
private function add_theme_sources() {
$theme = wp_get_theme();
$theme_dir = get_template_directory_uri();
// Add theme-specific sources based on common patterns
if (file_exists(get_template_directory() . '/assets/js/')) {
// Theme has custom JS
$this->directives['script-src'][] = $theme_dir;
}
// Check for common theme frameworks
if (strpos($theme->get('Name'), 'Divi') !== false) {
$this->directives['style-src'][] = "'unsafe-eval'";
}
}
public function send_csp_headers() {
$policy = $this->build_policy();
$header_name = $this->report_only ?
'Content-Security-Policy-Report-Only' :
'Content-Security-Policy';
header("$header_name: $policy");
// Add nonce to script tags
add_filter('script_loader_tag', [$this, 'add_nonce_to_scripts'], 10, 3);
add_filter('style_loader_tag', [$this, 'add_nonce_to_styles'], 10, 4);
}
private function build_policy() {
$policy_parts = [];
foreach ($this->directives as $directive => $sources) {
if (!empty($sources)) {
$policy_parts[] = $directive . ' ' . implode(' ', array_unique($sources));
}
}
return implode('; ', $policy_parts);
}
public function add_nonce_to_scripts($tag, $handle, $src) {
// Don't add nonce to external scripts
if (strpos($src, home_url()) === false && strpos($src, 'http') === 0) {
return $tag;
}
return str_replace(' src=', ' nonce="' . $this->nonce . '" src=', $tag);
}
public function add_nonce_to_styles($tag, $handle, $href, $media) {
// Don't add nonce to external styles
if (strpos($href, home_url()) === false && strpos($href, 'http') === 0) {
return $tag;
}
return str_replace(' href=', ' nonce="' . $this->nonce . '" href=', $tag);
}
public function handle_csp_report() {
$input = file_get_contents('php://input');
$report = json_decode($input, true);
if ($report && isset($report['csp-report'])) {
$this->log_csp_violation($report['csp-report']);
}
wp_die('', '', ['response' => 204]);
}
private function log_csp_violation($report) {
$log_entry = [
'timestamp' => current_time('mysql'),
'document_uri' => $report['document-uri'] ?? '',
'violated_directive' => $report['violated-directive'] ?? '',
'blocked_uri' => $report['blocked-uri'] ?? '',
'source_file' => $report['source-file'] ?? '',
'line_number' => $report['line-number'] ?? 0
];
// Log to database
global $wpdb;
$table_name = $wpdb->prefix . 'csp_violations';
$wpdb->insert($table_name, $log_entry);
// Also log to error log for debugging
error_log('CSP Violation: ' . json_encode($log_entry));
}
}
// Initialize the plugin
new WP_CSP_Manager();