PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.4
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.4
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-ssl.php

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

342 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Security Analyzer — SSL/TLS category (12 pts).
4 *
5 * Checks: ssl_present (2), ssl_cert_valid (2), ssl_cert_expiry (2),
6 * https_redirect (1), tls_version (1), mixed_content (2), force_ssl_admin (2).
7 *
8 * @package Vigilante
9 * @since 2.1.0
10 */
11
12 // Prevent direct access.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * SSL/TLS checks.
19 *
20 * Fast checks: is_ssl(), constants, settings.
21 * Slow checks: cert parsing, HTTP redirect probe, mixed content regex.
22 */
23 class Vigilante_SA_Category_SSL {
24
25 const SLUG = 'ssl';
26
27 /**
28 * @var Vigilante_Settings
29 */
30 private $settings;
31
32 public function __construct( Vigilante_Settings $settings ) {
33 $this->settings = $settings;
34 }
35
36 /**
37 * Run the category.
38 *
39 * @param string $phase 'fast' | 'slow' | 'all'.
40 * @return Vigilante_SA_Check_Result[]
41 */
42 public function run( $phase = 'all' ) {
43 $results = array();
44
45 if ( 'fast' === $phase || 'all' === $phase ) {
46 $results[] = $this->check_ssl_present();
47 $results[] = $this->check_force_ssl_admin();
48 }
49
50 if ( 'slow' === $phase || 'all' === $phase ) {
51 $cert = Vigilante_SA_Helpers::probe_tls();
52 $results[] = $this->check_ssl_cert_valid( $cert );
53 $results[] = $this->check_ssl_cert_expiry( $cert );
54 $results[] = $this->check_https_redirect();
55 $results[] = $this->check_tls_version( $cert );
56 $results[] = $this->check_mixed_content();
57 }
58
59 return $results;
60 }
61
62 private function check_ssl_present() {
63 $has_ssl = is_ssl();
64 $home_url = (string) get_option( 'home' );
65 $site_url = (string) get_option( 'siteurl' );
66 $urls_ok = 0 === strpos( $home_url, 'https://' ) && 0 === strpos( $site_url, 'https://' );
67
68 $args = array(
69 'id' => 'ssl_present',
70 'category' => self::SLUG,
71 'max' => 2,
72 'label' => __( 'HTTPS status', 'vigilante' ),
73 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-force-https' ),
74 );
75
76 if ( $has_ssl && $urls_ok ) {
77 $args['detail'] = __( 'Site uses HTTPS; both home and siteurl URLs start with https://.', 'vigilante' );
78 return Vigilante_SA_Check_Result::pass( $args );
79 }
80 if ( $has_ssl && ! $urls_ok ) {
81 $args['detail'] = __( 'HTTPS is active but one of the WordPress URLs (home/siteurl) still uses http://. Update them under Settings → General.', 'vigilante' );
82 return Vigilante_SA_Check_Result::warn( $args );
83 }
84 $args['detail'] = __( 'HTTPS is not active. Enable a TLS certificate in your host and force HTTPS redirection.', 'vigilante' );
85 return Vigilante_SA_Check_Result::fail( $args );
86 }
87
88 private function check_ssl_cert_valid( $cert ) {
89 $args = array(
90 'id' => 'ssl_cert_valid',
91 'category' => self::SLUG,
92 'max' => 2,
93 'label' => __( 'TLS certificate', 'vigilante' ),
94 'fix_link' => '',
95 );
96
97 if ( empty( $cert ) || empty( $cert['valid'] ) ) {
98 $args['detail'] = __( 'Could not read a valid TLS certificate for the public domain. Check your hosting or CDN.', 'vigilante' );
99 return Vigilante_SA_Check_Result::fail( $args );
100 }
101
102 $now = time();
103 if ( $cert['valid_from'] > 0 && $now < $cert['valid_from'] ) {
104 $args['detail'] = __( 'The certificate is not yet valid (notBefore in the future). Check the server clock.', 'vigilante' );
105 return Vigilante_SA_Check_Result::fail( $args );
106 }
107 if ( $cert['valid_to'] > 0 && $now > $cert['valid_to'] ) {
108 $args['detail'] = __( 'The certificate has expired. Renew it immediately in your host.', 'vigilante' );
109 return Vigilante_SA_Check_Result::fail( $args );
110 }
111
112 $args['detail'] = sprintf(
113 /* translators: 1: issuer CN, 2: subject CN */
114 __( 'Issued by %1$s for %2$s.', 'vigilante' ),
115 $cert['issuer'] ? $cert['issuer'] : __( '(unknown)', 'vigilante' ),
116 $cert['subject'] ? $cert['subject'] : __( '(unknown)', 'vigilante' )
117 );
118 $args['data'] = array(
119 'issuer' => $cert['issuer'],
120 'subject' => $cert['subject'],
121 );
122 return Vigilante_SA_Check_Result::pass( $args );
123 }
124
125 private function check_ssl_cert_expiry( $cert ) {
126 $args = array(
127 'id' => 'ssl_cert_expiry',
128 'category' => self::SLUG,
129 'max' => 2,
130 'label' => __( 'Certificate expiry', 'vigilante' ),
131 'fix_link' => '',
132 );
133
134 if ( empty( $cert ) || empty( $cert['valid'] ) || null === $cert['days_left'] ) {
135 $args['detail'] = __( 'Could not read the certificate expiry.', 'vigilante' );
136 return Vigilante_SA_Check_Result::skip( $args );
137 }
138
139 $days = (int) $cert['days_left'];
140 $args['data'] = array( 'days_left' => $days );
141
142 if ( $days < 0 ) {
143 $args['detail'] = sprintf(
144 /* translators: %d: days since expiry */
145 __( 'The certificate expired %d days ago.', 'vigilante' ),
146 abs( $days )
147 );
148 return Vigilante_SA_Check_Result::fail( $args );
149 }
150
151 if ( $days < 14 ) {
152 $args['detail'] = sprintf(
153 /* translators: %d: days remaining */
154 __( 'The certificate expires in %d days. Schedule renewal now.', 'vigilante' ),
155 $days
156 );
157 return Vigilante_SA_Check_Result::fail( $args );
158 }
159
160 if ( $days < 30 ) {
161 $args['detail'] = sprintf(
162 /* translators: %d: days remaining */
163 __( 'The certificate expires in %d days. Plan renewal soon.', 'vigilante' ),
164 $days
165 );
166 return Vigilante_SA_Check_Result::warn( $args );
167 }
168
169 $args['detail'] = sprintf(
170 /* translators: %d: days remaining */
171 __( '%d days until the certificate expires.', 'vigilante' ),
172 $days
173 );
174 return Vigilante_SA_Check_Result::pass( $args );
175 }
176
177 private function check_https_redirect() {
178 $args = array(
179 'id' => 'https_redirect',
180 'category' => self::SLUG,
181 'max' => 1,
182 'label' => __( 'HTTP → HTTPS redirect', 'vigilante' ),
183 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-force-https' ),
184 );
185
186 $http_url = set_url_scheme( home_url( '/' ), 'http' );
187 $response = Vigilante_SA_Helpers::get( $http_url, array( 'redirection' => 0 ) );
188
189 if ( is_wp_error( $response ) ) {
190 $args['detail'] = __( 'Could not probe the HTTP version of the site (firewall may be blocking self-requests).', 'vigilante' );
191 return Vigilante_SA_Check_Result::skip( $args );
192 }
193
194 $code = (int) wp_remote_retrieve_response_code( $response );
195 $location = (string) wp_remote_retrieve_header( $response, 'location' );
196
197 if ( $code >= 300 && $code < 400 && 0 === strpos( $location, 'https://' ) ) {
198 $args['detail'] = __( 'HTTP requests received a 3xx redirect to an https:// URL.', 'vigilante' );
199 return Vigilante_SA_Check_Result::pass( $args );
200 }
201
202 $args['detail'] = __( 'The site is reachable over plain HTTP without a redirect to HTTPS.', 'vigilante' );
203 return Vigilante_SA_Check_Result::fail( $args );
204 }
205
206 private function check_tls_version( $cert ) {
207 $args = array(
208 'id' => 'tls_version',
209 'category' => self::SLUG,
210 'max' => 1,
211 'label' => __( 'TLS version', 'vigilante' ),
212 'fix_link' => '',
213 );
214
215 if ( empty( $cert ) || empty( $cert['tls_version'] ) ) {
216 $args['detail'] = __( 'Could not determine the negotiated TLS version.', 'vigilante' );
217 return Vigilante_SA_Check_Result::skip( $args );
218 }
219
220 $version = $cert['tls_version'];
221 $args['data'] = array( 'tls_version' => $version );
222
223 if ( stripos( $version, 'TLSv1.3' ) !== false || stripos( $version, 'TLSv1.2' ) !== false ) {
224 $args['detail'] = sprintf(
225 /* translators: %s: TLS version like TLSv1.3 */
226 __( 'The server negotiated %s.', 'vigilante' ),
227 $version
228 );
229 return Vigilante_SA_Check_Result::pass( $args );
230 }
231
232 $args['detail'] = sprintf(
233 /* translators: %s: TLS version */
234 __( 'The server negotiated %s. Ask your host to disable TLS 1.0/1.1.', 'vigilante' ),
235 $version
236 );
237 return Vigilante_SA_Check_Result::fail( $args );
238 }
239
240 private function check_mixed_content() {
241 $args = array(
242 'id' => 'mixed_content',
243 'category' => self::SLUG,
244 'max' => 2,
245 'label' => __( 'Mixed content on the homepage', 'vigilante' ),
246 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'headers', 'vigilante-section-headers-main' ),
247 );
248
249 $fix_setting = (int) $this->settings->get_option( 'security_headers', 'fix_mixed_content', 0 );
250 $probe = Vigilante_SA_Helpers::probe_home();
251
252 if ( null === $probe ) {
253 $args['detail'] = __( 'Could not fetch the homepage to scan for insecure resources.', 'vigilante' );
254 return Vigilante_SA_Check_Result::skip( $args );
255 }
256
257 // Count http:// occurrences in src/href attributes on HTTPS pages only.
258 if ( ! is_ssl() ) {
259 $args['detail'] = __( 'HTTPS is not active yet, so mixed content cannot be evaluated.', 'vigilante' );
260 return Vigilante_SA_Check_Result::skip( $args );
261 }
262
263 $matches = array();
264 preg_match_all( '#(src|href)=["\']http://[^"\']+["\']#i', $probe['body'], $matches );
265 $count = isset( $matches[0] ) ? count( $matches[0] ) : 0;
266
267 // An enforced CSP with upgrade-insecure-requests makes the browser
268 // load every subrequest over HTTPS (external references included),
269 // so remaining http:// strings in the HTML are not mixed content in
270 // practice. Measured on the live response, not on settings: a CDN
271 // that strips the header must not count as protected. Report-Only
272 // policies do not upgrade, hence only the enforced header counts.
273 $csp_header = isset( $probe['headers']['content-security-policy'] ) ? $probe['headers']['content-security-policy'] : '';
274 if ( is_array( $csp_header ) ) {
275 $csp_header = implode( ', ', $csp_header );
276 }
277 $upgrades = ( false !== stripos( (string) $csp_header, 'upgrade-insecure-requests' ) );
278
279 $args['data'] = array(
280 'insecure_refs' => $count,
281 'fix_setting_active' => (bool) $fix_setting,
282 'csp_upgrades' => $upgrades,
283 );
284
285 if ( 0 === $count ) {
286 $args['detail'] = $fix_setting
287 ? __( 'No insecure references in the homepage HTML; mixed-content fix is enabled in settings.', 'vigilante' )
288 : __( 'No insecure references found in the homepage HTML.', 'vigilante' );
289 return Vigilante_SA_Check_Result::pass( $args );
290 }
291
292 if ( $upgrades ) {
293 $args['detail'] = sprintf(
294 /* translators: %d: number of insecure references */
295 __( '%d http:// references in the HTML, but the response sends a CSP upgrade-insecure-requests directive, so browsers load them over HTTPS.', 'vigilante' ),
296 $count
297 );
298 return Vigilante_SA_Check_Result::pass( $args );
299 }
300
301 if ( $fix_setting ) {
302 $args['detail'] = sprintf(
303 /* translators: %d: number of insecure references */
304 __( '%d http:// references still leaked despite the mixed-content fix being enabled. Inspect the theme or a plugin output.', 'vigilante' ),
305 $count
306 );
307 return Vigilante_SA_Check_Result::warn( $args );
308 }
309
310 $args['detail'] = sprintf(
311 /* translators: %d: number of insecure references */
312 __( '%d insecure http:// references found. Enable the mixed-content fix under Security Headers.', 'vigilante' ),
313 $count
314 );
315 return Vigilante_SA_Check_Result::fail( $args );
316 }
317
318 private function check_force_ssl_admin() {
319 $args = array(
320 'id' => 'force_ssl_admin',
321 'category' => self::SLUG,
322 'max' => 2,
323 'label' => __( 'FORCE_SSL_ADMIN constant', 'vigilante' ),
324 'fix_link' => Vigilante_SA_Helpers::build_fix_url( 'wp-hardening', 'field-force-ssl-admin' ),
325 );
326
327 $active = defined( 'FORCE_SSL_ADMIN' ) && FORCE_SSL_ADMIN;
328 if ( $active ) {
329 $args['detail'] = __( 'FORCE_SSL_ADMIN is defined as true; admin and login traffic requires HTTPS.', 'vigilante' );
330 return Vigilante_SA_Check_Result::pass( $args );
331 }
332
333 if ( ! is_ssl() ) {
334 $args['detail'] = __( 'HTTPS is not active yet, so enforcing SSL for the admin area is not applicable.', 'vigilante' );
335 return Vigilante_SA_Check_Result::skip( $args );
336 }
337
338 $args['detail'] = __( 'FORCE_SSL_ADMIN is not set. Enable it from the WP Hardening tab so admin and login cookies only travel over HTTPS.', 'vigilante' );
339 return Vigilante_SA_Check_Result::fail( $args );
340 }
341 }
342