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();