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.
Opens WPIDE on
Code
// 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 add this snippet
- Install WPIDE on your WordPress site (it is free) and open Code Snippets.
- Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
- It is added inactive: review the code and settings, then switch it on.