PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / includes / security-analyzer / class-sa-category-headers.php

class-sa-category-headers.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.9.8, at includes/security-analyzer/class-sa-category-headers.php

319 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Security Analyzer — HTTP headers category (18 pts).
4 *
5 * Reads the actual headers served from the homepage and cross-checks them
6 * with the Vigilante Security Headers settings. When a setting is enabled
7 * but the header is missing in the response the result is a WARN with the
8 * useful "configured but not applied" diagnostic.
9 *
10 * @package Vigilante
11 * @since 2.1.0
12 */
13
14 // Prevent direct access.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * HTTP security headers checks.
21 */
22 class Vigilante_SA_Category_Headers {
23
24 const SLUG = 'headers';
25
26 /**
27 * @var Vigilante_Settings
28 */
29 private $settings;
30
31 public function __construct( Vigilante_Settings $settings ) {
32 $this->settings = $settings;
33 }
34
35 /**
36 * Run the category. All checks need the home probe, so they're all in slow phase.
37 *
38 * @param string $phase 'fast' | 'slow' | 'all'.
39 * @return Vigilante_SA_Check_Result[]
40 */
41 public function run( $phase = 'all' ) {
42 if ( 'fast' === $phase ) {
43 return array();
44 }
45
46 $probe = Vigilante_SA_Helpers::probe_home();
47 $headers = ( $probe && isset( $probe['headers'] ) ) ? $probe['headers'] : array();
48 $sec_hdrs = $this->settings->get_section( 'security_headers' );
49
50 $results = array();
51 $results[] = $this->check_csp( $headers, $sec_hdrs );
52 $results[] = $this->check_hsts( $headers, $sec_hdrs );
53 $results[] = $this->check_x_frame( $headers );
54 $results[] = $this->check_x_content_type( $headers );
55 $results[] = $this->check_referrer_policy( $headers );
56 $results[] = $this->check_permissions_policy( $headers );
57 $results[] = $this->check_coop( $headers );
58 $results[] = $this->check_corp( $headers );
59 $results[] = $this->check_server_signature( $headers );
60
61 // If the probe failed entirely, mark all as SKIP.
62 if ( null === $probe ) {
63 foreach ( $results as $r ) {
64 $r->state = Vigilante_SA_Check_Result::STATE_SKIP;
65 $r->score = 0;
66 $r->detail = __( 'Could not fetch the homepage to read response headers.', 'vigilante' );
67 }
68 }
69
70 return $results;
71 }
72
73 private function check_csp( $headers, $sec_hdrs ) {
74 $args = array(
75 'id' => 'csp',
76 'category' => self::SLUG,
77 'max' => 4,
78 'label' => __( 'Content-Security-Policy', 'vigilante' ),
79 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-csp' ),
80 );
81
82 $setting_on = ! empty( $sec_hdrs['csp']['enabled'] );
83 $header = $this->header_value( $headers, array( 'content-security-policy', 'content-security-policy-report-only' ) );
84
85 if ( $header ) {
86 $args['detail'] = sprintf(
87 /* translators: %s: header value (truncated) */
88 __( 'CSP delivered: %s', 'vigilante' ),
89 $this->truncate( $header, 80 )
90 );
91 return Vigilante_SA_Check_Result::pass( $args );
92 }
93
94 if ( $setting_on ) {
95 $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' );
96 return Vigilante_SA_Check_Result::warn( $args );
97 }
98
99 $args['detail'] = __( 'Content-Security-Policy is not enabled. Turn it on under Security Headers → CSP.', 'vigilante' );
100 return Vigilante_SA_Check_Result::fail( $args );
101 }
102
103 private function check_hsts( $headers, $sec_hdrs ) {
104 $args = array(
105 'id' => 'hsts',
106 'category' => self::SLUG,
107 'max' => 3,
108 'label' => __( 'Strict-Transport-Security', 'vigilante' ),
109 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-hsts' ),
110 );
111
112 $setting_on = ! empty( $sec_hdrs['hsts']['enabled'] );
113 $header = $this->header_value( $headers, array( 'strict-transport-security' ) );
114
115 if ( $header ) {
116 $args['detail'] = sprintf(
117 /* translators: %s: HSTS header value */
118 __( 'HSTS header value: %s', 'vigilante' ),
119 $header
120 );
121 return Vigilante_SA_Check_Result::pass( $args );
122 }
123
124 if ( $setting_on ) {
125 $args['detail'] = __( 'HSTS is enabled in Vigilant but the header is not being served (often caused by caching layers or HTTP-level servers).', 'vigilante' );
126 return Vigilante_SA_Check_Result::warn( $args );
127 }
128
129 $args['detail'] = __( 'HSTS is disabled. Enable it after confirming HTTPS works correctly to prevent downgrade attacks.', 'vigilante' );
130 return Vigilante_SA_Check_Result::fail( $args );
131 }
132
133 private function check_x_frame( $headers ) {
134 $args = array(
135 'id' => 'x_frame',
136 'category' => self::SLUG,
137 'max' => 2,
138 'label' => __( 'X-Frame-Options', 'vigilante' ),
139 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
140 );
141
142 $header = $this->header_value( $headers, array( 'x-frame-options' ) );
143 if ( $header ) {
144 $args['detail'] = sprintf(
145 /* translators: %s: x-frame-options value */
146 __( 'X-Frame-Options header value: %s', 'vigilante' ),
147 $header
148 );
149 return Vigilante_SA_Check_Result::pass( $args );
150 }
151
152 $args['detail'] = __( 'X-Frame-Options header is missing. Attackers could embed your site in an iframe for click-jacking.', 'vigilante' );
153 return Vigilante_SA_Check_Result::fail( $args );
154 }
155
156 private function check_x_content_type( $headers ) {
157 $args = array(
158 'id' => 'x_content_type',
159 'category' => self::SLUG,
160 'max' => 2,
161 'label' => __( 'X-Content-Type-Options', 'vigilante' ),
162 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
163 );
164
165 $header = $this->header_value( $headers, array( 'x-content-type-options' ) );
166 if ( $header && stripos( $header, 'nosniff' ) !== false ) {
167 $args['detail'] = __( 'X-Content-Type-Options header set to nosniff.', 'vigilante' );
168 return Vigilante_SA_Check_Result::pass( $args );
169 }
170
171 $args['detail'] = __( 'X-Content-Type-Options: nosniff is missing. The browser may interpret files as a type other than declared.', 'vigilante' );
172 return Vigilante_SA_Check_Result::fail( $args );
173 }
174
175 private function check_referrer_policy( $headers ) {
176 $args = array(
177 'id' => 'referrer_policy',
178 'category' => self::SLUG,
179 'max' => 2,
180 'label' => __( 'Referrer-Policy', 'vigilante' ),
181 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
182 );
183
184 $header = $this->header_value( $headers, array( 'referrer-policy' ) );
185 if ( $header ) {
186 $args['detail'] = sprintf(
187 /* translators: %s: referrer policy value */
188 __( 'Referrer-Policy: %s', 'vigilante' ),
189 $header
190 );
191 return Vigilante_SA_Check_Result::pass( $args );
192 }
193
194 $args['detail'] = __( 'Referrer-Policy header is missing. The browser decides what to leak in the Referer by default.', 'vigilante' );
195 return Vigilante_SA_Check_Result::fail( $args );
196 }
197
198 private function check_permissions_policy( $headers ) {
199 $args = array(
200 'id' => 'permissions_policy',
201 'category' => self::SLUG,
202 'max' => 2,
203 'label' => __( 'Permissions-Policy', 'vigilante' ),
204 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
205 );
206
207 $header = $this->header_value( $headers, array( 'permissions-policy', 'feature-policy' ) );
208 if ( $header ) {
209 $args['detail'] = sprintf(
210 /* translators: %s: header value (truncated) */
211 __( 'Permissions-Policy header value: %s', 'vigilante' ),
212 $this->truncate( $header, 80 )
213 );
214 return Vigilante_SA_Check_Result::pass( $args );
215 }
216
217 $args['detail'] = __( 'Permissions-Policy is missing. Browsers allow all capabilities (geolocation, camera, etc.) by default.', 'vigilante' );
218 return Vigilante_SA_Check_Result::fail( $args );
219 }
220
221 private function check_coop( $headers ) {
222 $args = array(
223 'id' => 'coop',
224 'category' => self::SLUG,
225 'max' => 1,
226 'label' => __( 'Cross-Origin-Opener-Policy', 'vigilante' ),
227 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-cross-origin' ),
228 );
229
230 $header = $this->header_value( $headers, array( 'cross-origin-opener-policy' ) );
231 if ( $header ) {
232 /* translators: %s: value of the Cross-Origin-Opener-Policy HTTP header */
233 $args['detail'] = sprintf( __( 'COOP: %s', 'vigilante' ), $header );
234 return Vigilante_SA_Check_Result::pass( $args );
235 }
236
237 $args['detail'] = __( 'Cross-Origin-Opener-Policy is missing. Recommended to isolate the browsing context.', 'vigilante' );
238 return Vigilante_SA_Check_Result::fail( $args );
239 }
240
241 private function check_corp( $headers ) {
242 $args = array(
243 'id' => 'corp',
244 'category' => self::SLUG,
245 'max' => 1,
246 'label' => __( 'Cross-Origin-Resource-Policy', 'vigilante' ),
247 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-cross-origin' ),
248 );
249
250 $header = $this->header_value( $headers, array( 'cross-origin-resource-policy' ) );
251 if ( $header ) {
252 /* translators: %s: value of the Cross-Origin-Resource-Policy HTTP header */
253 $args['detail'] = sprintf( __( 'CORP: %s', 'vigilante' ), $header );
254 return Vigilante_SA_Check_Result::pass( $args );
255 }
256
257 $args['detail'] = __( 'Cross-Origin-Resource-Policy is missing. Cross-origin fetches are unrestricted.', 'vigilante' );
258 return Vigilante_SA_Check_Result::fail( $args );
259 }
260
261 private function check_server_signature( $headers ) {
262 $args = array(
263 'id' => 'server_signature',
264 'category' => self::SLUG,
265 'max' => 1,
266 'label' => __( 'Server fingerprint exposure', 'vigilante' ),
267 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-fingerprint' ),
268 );
269
270 $server = $this->header_value( $headers, array( 'server' ) );
271 $x_powered = $this->header_value( $headers, array( 'x-powered-by' ) );
272 $leaks = array();
273
274 if ( $server && preg_match( '#[\d\.]+#', $server ) ) {
275 $leaks[] = 'Server: ' . $server;
276 }
277 if ( $x_powered ) {
278 $leaks[] = 'X-Powered-By: ' . $x_powered;
279 }
280
281 if ( empty( $leaks ) ) {
282 $args['detail'] = __( 'No server or PHP version information found in response headers.', 'vigilante' );
283 return Vigilante_SA_Check_Result::pass( $args );
284 }
285
286 $args['detail'] = sprintf(
287 /* translators: %s: comma-separated leaked headers */
288 __( 'Version information leaked through response headers: %s', 'vigilante' ),
289 implode( ' | ', $leaks )
290 );
291 return Vigilante_SA_Check_Result::fail( $args );
292 }
293
294 /**
295 * Read a header value from the normalized map, trying multiple key variants.
296 *
297 * @param array<string,string> $headers Normalized lowercase headers.
298 * @param string[] $keys Candidate keys.
299 * @return string Value or empty string.
300 */
301 private function header_value( $headers, $keys ) {
302 foreach ( $keys as $key ) {
303 $k = strtolower( $key );
304 if ( isset( $headers[ $k ] ) && '' !== trim( $headers[ $k ] ) ) {
305 return $headers[ $k ];
306 }
307 }
308 return '';
309 }
310
311 private function truncate( $text, $len ) {
312 $text = (string) $text;
313 if ( strlen( $text ) <= $len ) {
314 return $text;
315 }
316 return substr( $text, 0, $len ) . '';
317 }
318 }
319