# Content Security Policy Builder With Violation Reports

> Sends a report-only Content Security Policy and collects the browser's violation reports, then lists every blocked source per directive under Tools > CSP Reports with a suggested policy you can enforce later.

- Type: PHP
- Runs: Run everywhere
- Category: [Security](https://wpide.com/library/category/security/)
- Tags: csp, headers, reports
- Author: WPIDE Team
- Installs: 0
- Updated: 2026-10-28
- Page: https://wpide.com/library/sec-csp-report-collector/

## Code

```php
// Starting policy (report-only). Sources reported as blocked are suggested additions.
$wpide_csp_policy = array(
    'default-src' => "'self'",
    'script-src'  => "'self' 'unsafe-inline'",
    'style-src'   => "'self' 'unsafe-inline'",
    'img-src'     => "'self' data:",
    'font-src'    => "'self' data:",
    'connect-src' => "'self'",
    'frame-src'   => "'self'",
    'object-src'  => "'none'",
    'base-uri'    => "'self'",
);

add_action( 'send_headers', function () use ( $wpide_csp_policy ) {
    if ( headers_sent() || is_user_logged_in() ) {
        return;
    }

    $parts = array();
    foreach ( $wpide_csp_policy as $directive => $sources ) {
        $parts[] = $directive . ' ' . $sources;
    }
    $parts[] = 'report-uri ' . esc_url_raw( rest_url( 'wpide/v1/csp-report' ) );

    header( 'Content-Security-Policy-Report-Only: ' . implode( '; ', $parts ) );
} );

add_action( 'rest_api_init', function () {
    register_rest_route( 'wpide/v1', '/csp-report', array(
        'methods'             => 'POST',
        'permission_callback' => '__return_true',
        'callback'            => function ( WP_REST_Request $request ) {
            $ip   = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
            $rate = 'wpide_csp_rate_' . md5( $ip );
            if ( (int) get_transient( $rate ) > 60 ) {
                return new WP_REST_Response( null, 204 );
            }
            set_transient( $rate, (int) get_transient( $rate ) + 1, MINUTE_IN_SECONDS );

            $body    = json_decode( (string) $request->get_body(), true );
            $reports = isset( $body['csp-report'] ) ? array( $body['csp-report'] ) : ( is_array( $body ) ? wp_list_pluck( array_filter( $body, 'is_array' ), 'body' ) : array() );
            $store   = (array) get_option( 'wpide_csp_reports', array() );

            foreach ( $reports as $report ) {
                if ( ! is_array( $report ) ) {
                    continue;
                }
                $directive = isset( $report['effective-directive'] ) ? $report['effective-directive'] : ( isset( $report['effectiveDirective'] ) ? $report['effectiveDirective'] : ( isset( $report['violated-directive'] ) ? $report['violated-directive'] : '' ) );
                $blocked   = isset( $report['blocked-uri'] ) ? $report['blocked-uri'] : ( isset( $report['blockedURL'] ) ? $report['blockedURL'] : '' );
                $directive = sanitize_key( strtok( (string) $directive, ' ' ) );
                $host      = wp_parse_url( (string) $blocked, PHP_URL_HOST );
                $source    = $host ? 'https://' . strtolower( $host ) : sanitize_key( (string) $blocked );

                if ( ! $directive || ! $source ) {
                    continue;
                }
                $key = $directive . '|' . $source;
                if ( ! isset( $store[ $key ] ) && count( $store ) >= 200 ) {
                    continue;
                }
                $store[ $key ] = array( 'count' => ( isset( $store[ $key ]['count'] ) ? (int) $store[ $key ]['count'] : 0 ) + 1, 'last' => time() );
            }

            update_option( 'wpide_csp_reports', $store, false );

            return new WP_REST_Response( null, 204 );
        },
    ) );
} );

add_action( 'admin_menu', function () use ( $wpide_csp_policy ) {
    add_management_page( 'CSP Reports', 'CSP Reports', 'manage_options', 'wpide-csp-reports', function () use ( $wpide_csp_policy ) {
        if ( isset( $_POST['wpide_csp_clear'] ) && check_admin_referer( 'wpide_csp_clear' ) ) {
            delete_option( 'wpide_csp_reports' );
        }

        $store   = (array) get_option( 'wpide_csp_reports', array() );
        $suggest = $wpide_csp_policy;

        echo '<div class="wrap"><h1>CSP Reports</h1><table class="widefat striped"><thead><tr><th>Directive</th><th>Blocked source</th><th>Reports</th><th>Last seen</th></tr></thead><tbody>';
        if ( ! $store ) {
            echo '<tr><td colspan="4">No violations reported yet.</td></tr>';
        }
        foreach ( $store as $key => $row ) {
            list( $directive, $source ) = explode( '|', $key, 2 );
            $target = isset( $suggest[ $directive ] ) ? $directive : 'default-src';
            if ( false === strpos( $suggest[ $target ], $source ) && in_array( $source, array( 'inline', 'eval', 'data', 'blob' ), true ) === false ) {
                $suggest[ $target ] .= ' ' . $source;
            }
            printf( '<tr><td><code>%s</code></td><td>%s</td><td>%d</td><td>%s</td></tr>', esc_html( $directive ), esc_html( $source ), (int) $row['count'], esc_html( human_time_diff( (int) $row['last'] ) . ' ago' ) );
        }
        echo '</tbody></table><h2>Suggested policy</h2><textarea readonly rows="6" class="large-text code">';
        $lines = array();
        foreach ( $suggest as $directive => $sources ) {
            $lines[] = $directive . ' ' . $sources;
        }
        echo esc_textarea( implode( '; ', $lines ) );
        echo '</textarea><form method="post">';
        wp_nonce_field( 'wpide_csp_clear' );
        submit_button( 'Clear reports', 'secondary', 'wpide_csp_clear' );
        echo '</form></div>';
    } );
} );
```

## Notes

Let it run for a few days while you browse the whole site, then copy the suggested policy into your security headers; reports are stored in one capped option.

## How to use it

Install the free WPIDE plugin (https://wordpress.org/plugins/wpide/), find "Content Security Policy Builder With Violation Reports" in its snippet library and add it in one click; or create a new PHP snippet in WPIDE (or any code snippets plugin) and paste the code above. More snippets: https://wpide.com/library/
