# vigilante/2.10.1/includes/security-analyzer/class-sa-category-headers.php

Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner…, version 2.10.1. 401 lines.

- Page: https://pluginprobe.com/plugins/vigilante/2.10.1/code/includes/security-analyzer/class-sa-category-headers.php
- Raw: https://pluginprobe.com/plugins/vigilante/2.10.1/raw/includes/security-analyzer/class-sa-category-headers.php
- Modified: 2026-08-26T12:37:56+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/vigilante/2.10.1/code/includes/security-analyzer/class-sa-category-headers.php#L10-L20`.

```php
<?php
/**
 * Security Analyzer — HTTP headers category (18 pts).
 *
 * Reads the actual headers served from the homepage and cross-checks them
 * with the Vigilante Security Headers settings. When a setting is enabled
 * but the header is missing in the response the result is a WARN with the
 * useful "configured but not applied" diagnostic.
 *
 * @package Vigilante
 * @since   2.1.0
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * HTTP security headers checks.
 */
class Vigilante_SA_Category_Headers {

    const SLUG = 'headers';

    /**
     * @var Vigilante_Settings
     */
    private $settings;

    public function __construct( Vigilante_Settings $settings ) {
        $this->settings = $settings;
    }

    /**
     * Run the category. All checks need the home probe, so they're all in slow phase.
     *
     * @param string $phase 'fast' | 'slow' | 'all'.
     * @return Vigilante_SA_Check_Result[]
     */
    public function run( $phase = 'all' ) {
        if ( 'fast' === $phase ) {
            return array();
        }

        $probe    = Vigilante_SA_Helpers::probe_home();
        $headers  = ( $probe && isset( $probe['headers'] ) ) ? $probe['headers'] : array();
        $sec_hdrs = $this->settings->get_section( 'security_headers' );

        $results = array();
        $results[] = $this->check_csp( $headers, $sec_hdrs );
        $results[] = $this->check_hsts( $headers, $sec_hdrs );
        $results[] = $this->check_x_frame( $headers );
        $results[] = $this->check_x_content_type( $headers );
        $results[] = $this->check_referrer_policy( $headers );
        $results[] = $this->check_permissions_policy( $headers );
        $results[] = $this->check_coop( $headers, $sec_hdrs );
        $results[] = $this->check_corp( $headers, $sec_hdrs );
        $results[] = $this->check_server_signature( $headers );

        // If the probe failed entirely, mark all as SKIP.
        if ( null === $probe ) {
            foreach ( $results as $r ) {
                $r->state  = Vigilante_SA_Check_Result::STATE_SKIP;
                $r->score  = 0;
                $r->detail = __( 'Could not fetch the homepage to read response headers.', 'vigilante' );
            }
        }

        return $results;
    }

    private function check_csp( $headers, $sec_hdrs ) {
        $args = array(
            'id'       => 'csp',
            'category' => self::SLUG,
            'max'      => 4,
            'label'    => __( 'Content-Security-Policy', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-csp' ),
        );

        $setting_on = ! empty( $sec_hdrs['csp']['enabled'] );
        $header     = $this->header_value( $headers, array( 'content-security-policy', 'content-security-policy-report-only' ) );

        if ( $header ) {
            // A policy is arriving that Vigilant is not sending. Usually another
            // plugin or the server itself, which is fine. But it is also what a
            // site in a subfolder gets from the block a Vigilant installed one
            // directory up left behind, and then the settings screen and the
            // browser disagree with no way to tell why from here.
            if ( ! $setting_on ) {
                $ghosts = $this->vigilant_blocks_above();

                if ( ! empty( $ghosts ) ) {
                    $args['detail'] = sprintf(
                        /* translators: 1: CSP header value (truncated), 2: absolute path of the .htaccess file. */
                        __( 'A Content-Security-Policy is reaching the browser (%1$s) while CSP is off in Vigilant. It comes from a Vigilant block in %2$s, a directory above this site: Apache applies it here too, and this installation neither sees nor controls it. Remove that block, or manage the policy from the site that owns it.', 'vigilante' ),
                        $this->truncate( $header, 60 ),
                        implode( ', ', array_keys( $ghosts ) )
                    );
                    return Vigilante_SA_Check_Result::warn( $args );
                }

                $args['detail'] = sprintf(
                    /* translators: %s: header value (truncated) */
                    __( 'CSP delivered by something other than Vigilant: %s', 'vigilante' ),
                    $this->truncate( $header, 80 )
                );
                return Vigilante_SA_Check_Result::pass( $args );
            }

            $args['detail'] = sprintf(
                /* translators: %s: header value (truncated) */
                __( 'CSP delivered: %s', 'vigilante' ),
                $this->truncate( $header, 80 )
            );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        if ( $setting_on ) {
            $args['detail'] = __( 'CSP is enabled in Vigilant but the Content-Security-Policy header is not reaching the browser. A server rule or CDN is likely stripping it.', 'vigilante' );
            return Vigilante_SA_Check_Result::warn( $args );
        }

        $args['detail'] = __( 'Content-Security-Policy is not enabled. Turn it on under Security Headers → CSP.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    private function check_hsts( $headers, $sec_hdrs ) {
        $args = array(
            'id'       => 'hsts',
            'category' => self::SLUG,
            'max'      => 3,
            'label'    => __( 'Strict-Transport-Security', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-hsts' ),
        );

        $setting_on = ! empty( $sec_hdrs['hsts']['enabled'] );
        $header     = $this->header_value( $headers, array( 'strict-transport-security' ) );

        if ( $header ) {
            $args['detail'] = sprintf(
                /* translators: %s: HSTS header value */
                __( 'HSTS header value: %s', 'vigilante' ),
                $header
            );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        if ( $setting_on ) {
            $args['detail'] = __( 'HSTS is enabled in Vigilant but the header is not being served (often caused by caching layers or HTTP-level servers).', 'vigilante' );
            return Vigilante_SA_Check_Result::warn( $args );
        }

        $args['detail'] = __( 'HSTS is disabled. Enable it after confirming HTTPS works correctly to prevent downgrade attacks.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    private function check_x_frame( $headers ) {
        $args = array(
            'id'       => 'x_frame',
            'category' => self::SLUG,
            'max'      => 2,
            'label'    => __( 'X-Frame-Options', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
        );

        $header = $this->header_value( $headers, array( 'x-frame-options' ) );
        if ( $header ) {
            $args['detail'] = sprintf(
                /* translators: %s: x-frame-options value */
                __( 'X-Frame-Options header value: %s', 'vigilante' ),
                $header
            );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        $args['detail'] = __( 'X-Frame-Options header is missing. Attackers could embed your site in an iframe for click-jacking.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    private function check_x_content_type( $headers ) {
        $args = array(
            'id'       => 'x_content_type',
            'category' => self::SLUG,
            'max'      => 2,
            'label'    => __( 'X-Content-Type-Options', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
        );

        $header = $this->header_value( $headers, array( 'x-content-type-options' ) );
        if ( $header && stripos( $header, 'nosniff' ) !== false ) {
            $args['detail'] = __( 'X-Content-Type-Options header set to nosniff.', 'vigilante' );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        $args['detail'] = __( 'X-Content-Type-Options: nosniff is missing. The browser may interpret files as a type other than declared.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    private function check_referrer_policy( $headers ) {
        $args = array(
            'id'       => 'referrer_policy',
            'category' => self::SLUG,
            'max'      => 2,
            'label'    => __( 'Referrer-Policy', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
        );

        $header = $this->header_value( $headers, array( 'referrer-policy' ) );
        if ( $header ) {
            $args['detail'] = sprintf(
                /* translators: %s: referrer policy value */
                __( 'Referrer-Policy: %s', 'vigilante' ),
                $header
            );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        $args['detail'] = __( 'Referrer-Policy header is missing. The browser decides what to leak in the Referer by default.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    private function check_permissions_policy( $headers ) {
        $args = array(
            'id'       => 'permissions_policy',
            'category' => self::SLUG,
            'max'      => 2,
            'label'    => __( 'Permissions-Policy', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
        );

        $header = $this->header_value( $headers, array( 'permissions-policy', 'feature-policy' ) );
        if ( $header ) {
            $args['detail'] = sprintf(
                /* translators: %s: header value (truncated) */
                __( 'Permissions-Policy header value: %s', 'vigilante' ),
                $this->truncate( $header, 80 )
            );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        $args['detail'] = __( 'Permissions-Policy is missing. Browsers allow all capabilities (geolocation, camera, etc.) by default.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    /**
     * Cross-Origin-Opener-Policy.
     *
     * Switched off on purpose from the Headers tab is a legitimate configuration,
     * not a finding: COOP severs the window.opener link when another origin opens
     * this site, which is exactly what external tools such as Google Tag Assistant
     * rely on. Those runs report INFO so the check stays visible without dragging
     * the score down. WARN could not do that job: with a max of 1 it scores
     * floor( 1 / 2 ) = 0, the very same zero as a FAIL, and the grade scale keeps
     * "A" for a perfect 100/100.
     *
     * @param array $headers  Response headers from the home page probe.
     * @param array $sec_hdrs The security_headers settings section.
     * @return Vigilante_SA_Check_Result
     */
    private function check_coop( $headers, $sec_hdrs ) {
        $args = array(
            'id'       => 'coop',
            'category' => self::SLUG,
            'max'      => 1,
            'label'    => __( 'Cross-Origin-Opener-Policy', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-cross-origin' ),
        );

        $header = $this->header_value( $headers, array( 'cross-origin-opener-policy' ) );

        if ( $header && 'unsafe-none' !== strtolower( trim( $header ) ) ) {
            /* translators: %s: value of the Cross-Origin-Opener-Policy HTTP header */
            $args['detail'] = sprintf( __( 'COOP: %s', 'vigilante' ), $header );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        // No header at all, or an explicit unsafe-none. Was that this site's choice?
        $policies   = ( isset( $sec_hdrs['cross_origin_policies'] ) && is_array( $sec_hdrs['cross_origin_policies'] ) ) ? $sec_hdrs['cross_origin_policies'] : array();
        $configured = isset( $policies['opener_policy'] ) ? (string) $policies['opener_policy'] : '';

        if ( '' === $configured || 'unsafe-none' === $configured ) {
            $args['detail'] = __( 'Cross-Origin-Opener-Policy is switched off in Vigilant. Windows opened by other sites keep their link to yours, which is what external tools such as Google Tag Assistant need. Not scored.', 'vigilante' );
            return Vigilante_SA_Check_Result::info( $args );
        }

        $args['detail'] = __( 'Cross-Origin-Opener-Policy is missing. Recommended to isolate the browsing context.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    /**
     * Cross-Origin-Resource-Policy. Same reasoning as check_coop() for the INFO state.
     *
     * @param array $headers  Response headers from the home page probe.
     * @param array $sec_hdrs The security_headers settings section.
     * @return Vigilante_SA_Check_Result
     */
    private function check_corp( $headers, $sec_hdrs ) {
        $args = array(
            'id'       => 'corp',
            'category' => self::SLUG,
            'max'      => 1,
            'label'    => __( 'Cross-Origin-Resource-Policy', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-cross-origin' ),
        );

        $header = $this->header_value( $headers, array( 'cross-origin-resource-policy' ) );
        if ( $header ) {
            /* translators: %s: value of the Cross-Origin-Resource-Policy HTTP header */
            $args['detail'] = sprintf( __( 'CORP: %s', 'vigilante' ), $header );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        $policies   = ( isset( $sec_hdrs['cross_origin_policies'] ) && is_array( $sec_hdrs['cross_origin_policies'] ) ) ? $sec_hdrs['cross_origin_policies'] : array();
        $configured = isset( $policies['resource_policy'] ) ? (string) $policies['resource_policy'] : '';

        if ( '' === $configured ) {
            $args['detail'] = __( 'Cross-Origin-Resource-Policy is switched off in Vigilant, so other sites may load your images and files as usual. Not scored.', 'vigilante' );
            return Vigilante_SA_Check_Result::info( $args );
        }

        $args['detail'] = __( 'Cross-Origin-Resource-Policy is missing. Cross-origin fetches are unrestricted.', 'vigilante' );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    private function check_server_signature( $headers ) {
        $args = array(
            'id'       => 'server_signature',
            'category' => self::SLUG,
            'max'      => 1,
            'label'    => __( 'Server fingerprint exposure', 'vigilante' ),
            'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-fingerprint' ),
        );

        $server      = $this->header_value( $headers, array( 'server' ) );
        $x_powered   = $this->header_value( $headers, array( 'x-powered-by' ) );
        $leaks       = array();

        if ( $server && preg_match( '#[\d\.]+#', $server ) ) {
            $leaks[] = 'Server: ' . $server;
        }
        if ( $x_powered ) {
            $leaks[] = 'X-Powered-By: ' . $x_powered;
        }

        if ( empty( $leaks ) ) {
            $args['detail'] = __( 'No server or PHP version information found in response headers.', 'vigilante' );
            return Vigilante_SA_Check_Result::pass( $args );
        }

        $args['detail'] = sprintf(
            /* translators: %s: comma-separated leaked headers */
            __( 'Version information leaked through response headers: %s', 'vigilante' ),
            implode( ' | ', $leaks )
        );
        return Vigilante_SA_Check_Result::fail( $args );
    }

    /**
     * Read a header value from the normalized map, trying multiple key variants.
     *
     * @param array<string,string> $headers Normalized lowercase headers.
     * @param string[]             $keys    Candidate keys.
     * @return string Value or empty string.
     */
    private function header_value( $headers, $keys ) {
        foreach ( $keys as $key ) {
            $k = strtolower( $key );
            if ( isset( $headers[ $k ] ) && '' !== trim( $headers[ $k ] ) ) {
                return $headers[ $k ];
            }
        }
        return '';
    }

    private function truncate( $text, $len ) {
        $text = (string) $text;
        if ( strlen( $text ) <= $len ) {
            return $text;
        }
        return substr( $text, 0, $len ) . '…';
    }

    /**
     * Vigilant blocks found in .htaccess files above this installation
     *
     * @since 2.9.9
     *
     * @return array<string,string[]> Absolute file path => markers found inside.
     */
    private function vigilant_blocks_above() {
        if ( ! class_exists( 'Vigilante_Htaccess_Manager' ) ) {
            require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php';
        }

        return Vigilante_Htaccess_Manager::get_instance()->find_blocks_above();
    }

}

```
