PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / security-headers / class-sh-strict-transport.php

class-sh-strict-transport.php in Defender Security – Malware Scanner, Login Security & Firewall 5.8.1, at src/component/security-headers/class-sh-strict-transport.php

293 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the implementation of the strict-transport-security header for security purposes.
4 *
5 * @package WP_Defender\Component\Security_Headers
6 */
7
8 namespace WP_Defender\Component\Security_Headers;
9
10 use WP_Defender\Component\Security_Header;
11
12 /**
13 * Manages the strict-transport-security header which controls browser features.
14 */
15 class Sh_Strict_Transport extends Security_Header {
16
17 /**
18 * Unique identifier for this security header rule.
19 *
20 * @var string
21 */
22 public static $rule_slug = 'sh_strict_transport';
23
24 /**
25 * Get time in seconds
26 *
27 * @return array
28 */
29 private function time_in_seconds() {
30 return array(
31 '1 hour' => 1 * 3600,
32 '24 hours' => 86400,
33 '7 days' => 7 * 86400,
34 '30 days' => 30 * 86400,
35 '3 months' => ( 3 * 30 + 1 ) * 86400,
36 '6 months' => ( 6 * 30 + 3 ) * 86400,
37 '1 year' => 365 * 86400,
38 '2 years' => 365 * 2 * 86400,
39 );
40 }
41
42 /**
43 * Check HTTPS
44 *
45 * @return bool
46 */
47 private function is_https() {
48 return isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'];
49 }
50
51 /**
52 * Checks if the Policy should be applied based on the current settings and site configuration.
53 *
54 * @return bool True if the header should be applied, false otherwise.
55 */
56 public function check() {
57 $model = $this->get_model();
58
59 if ( ! $model->sh_strict_transport ) {
60 return false;
61 }
62 // 'max-age' directive is required
63 if ( '' !== $model->hsts_cache_duration ) {
64 return true;
65 }
66 $headers = $this->head_request( network_site_url(), self::$rule_slug );
67 if ( is_wp_error( $headers ) ) {
68 $this->log( sprintf( 'Self ping error: %s', $headers->get_error_message() ), wd_internal_log() );
69
70 return false;
71 }
72
73 if ( isset( $headers['strict-transport-security'] ) ) {
74 $hsts_cache_duration = '';
75 $hsts_preload = 0;
76 $include_subdomain = 0;
77 $header_sts = is_array( $headers['strict-transport-security'] )
78 ? $headers['strict-transport-security'][0]
79 : $headers['strict-transport-security'];
80
81 $content = explode( ';', $header_sts );
82 foreach ( $content as $line ) {
83 if ( stristr( $line, 'max-age' ) ) {
84 $value = explode( '=', $line );
85 $arr = $this->time_in_seconds();
86 $seconds = isset( $value[1] ) ? (int) $value[1] : 0;
87 $closest = null;
88 $key = null;
89 foreach ( $arr as $k => $item ) {
90 if ( is_null( $closest ) || ( ! is_null( $closest ) && ( abs( $seconds - $closest ) > abs( $item - $seconds ) ) ) ) {
91 $closest = $item;
92 $key = $k;
93 }
94 }
95 $hsts_cache_duration = $key;
96 } elseif ( stristr( $line, 'preload' ) ) {
97 $hsts_preload = 1;
98 } elseif ( stristr( $line, 'includeSubDomains' ) ) {
99 $include_subdomain = 1;
100 }
101 }
102
103 if ( ( '' !== $hsts_cache_duration )
104 || ( 0 !== $hsts_preload )
105 || ( 0 !== $include_subdomain )
106 ) {
107 if ( is_null( $model->hsts_preload ) && $hsts_preload ) {
108 $model->hsts_preload = $hsts_preload;
109 }
110 if ( is_null( $model->include_subdomain ) && $include_subdomain ) {
111 $model->include_subdomain = $include_subdomain;
112 }
113 if ( is_null( $model->hsts_cache_duration ) && $hsts_cache_duration ) {
114 $model->hsts_cache_duration = $hsts_cache_duration;
115 }
116 $model->save();
117 }
118
119 return true;
120 }
121
122 return false;
123 }
124
125 /**
126 * Extracts the domain suffix from a given domain using a list of public suffixes.
127 *
128 * @param string $domain The domain from which to extract the suffix.
129 *
130 * @return string Returns the most appropriate domain suffix if found, otherwise false.
131 */
132 private function get_domain_suffix( $domain ): string {
133 require_once dirname( __DIR__ ) . '/public-suffix.php';
134 $tlds = get_public_suffix();
135 // whitelist development.
136 $tlds['localhost'] = 1;
137 $parts = explode( '.', $domain );
138 $parts = array_reverse( $parts );
139 $suffix = '';
140 $collection = array();
141 $length = 0;
142 foreach ( $parts as $part ) {
143 $suffix = rtrim( $part . '.' . $suffix, '.' );
144 $not_allow = '!' . $suffix;
145 if ( isset( $tlds[ $not_allow ] ) ) {
146 // this wont be here.
147 continue;
148 }
149 if ( isset( $tlds[ $suffix ] ) ) {
150 if ( $length > strlen( $suffix ) ) {
151 // put at last.
152 $collection[] = $suffix;
153 } else {
154 array_unshift( $collection, $suffix );
155 }
156 }
157 }
158 if ( array() === $collection ) {
159 return '';
160 }
161
162 return $collection[0];
163 }
164
165 /**
166 * Parses the domain to extract structural components such as the host, top-level domain (TLD), and subdomain.
167 *
168 * @param string $domain The domain URL to parse.
169 *
170 * @return array|false Returns an array with keys 'host', 'tld', and 'subdomain' if parsing is successful,
171 * otherwise false.
172 */
173 public function parse_domain( $domain ) {
174 $filter_domain = version_compare( PHP_VERSION, '7.0', '>=' ) ? FILTER_VALIDATE_DOMAIN : FILTER_VALIDATE_URL;
175 if ( ! filter_var( $domain, $filter_domain ) ) {
176 return false;
177 }
178 $suffix = $this->get_domain_suffix( $domain );
179 if ( '' === $suffix ) {
180 return false;
181 }
182 // exclude 'www.'.
183 $domain = str_replace( 'www.', '', $domain );
184 $host = wp_parse_url( $domain, PHP_URL_HOST );
185 $host_without_tld = str_replace( $suffix, '', $host );
186
187 $host_without_tld = rtrim( $host_without_tld, '.' );
188 $parts = explode( '.', $host_without_tld );
189 if ( 1 === count( $parts ) ) {
190 return array(
191 'host' => $host,
192 'tld' => $suffix,
193 );
194 }
195 // parse to get the root & subdomain.
196 $domain = array_pop( $parts );
197
198 return array(
199 'host' => $host,
200 'tld' => $suffix,
201 'subdomain' => str_replace( $domain, '', $host_without_tld ),
202 );
203 }
204
205 /**
206 * Retrieves miscellaneous data related to the Policy.
207 *
208 * @return array Contains introductory text, mode, and values for the Policy.
209 */
210 public function get_misc_data() {
211 $model = $this->get_model();
212 $site_url = network_site_url();
213 $domain_data = $this->parse_domain( $site_url );
214 $allow_subdomain = false;
215
216 if ( is_array( $domain_data ) && ! isset( $domain_data['subdomain'] ) ) {
217 $allow_subdomain = true;
218 } elseif ( ! $domain_data && ! is_multisite() ) {
219 // case if a single site installs in a folder, e.g. http://example.com/something/folder/.
220 $allow_subdomain = true;
221 } elseif ( ! $domain_data && is_multisite() && is_subdomain_install() && is_main_site() ) {
222 // case if main site on MU with subdomain install.
223 $allow_subdomain = true;
224 }
225
226 return array(
227 'intro_text' => esc_html__(
228 'The HTTP Strict-Transport-Security response header (HSTS) lets a web site tell browsers that it should only be accessed using HTTPS, instead of using HTTP. This is extremely important for websites that store and process sensitive information like ECommerce stores and helps prevent Protocol Downgrade and Clickjacking attacks.',
229 'defender-security'
230 ),
231 'hsts_preload' => $model->hsts_preload ?? 0,
232 'include_subdomain' => $model->include_subdomain ?? 0,
233 'hsts_cache_duration' => $model->hsts_cache_duration ?? '30 days',
234 'allow_subdomain' => $allow_subdomain,
235 );
236 }
237
238 /**
239 * Registers hooks related to sending headers.
240 */
241 public function add_hooks() {
242 add_action( 'send_headers', array( $this, 'append_header' ) );
243 }
244
245 /**
246 * Appends the header to the response.
247 */
248 public function append_header() {
249 if ( headers_sent() ) {
250 return;
251 }
252
253 if ( ! $this->maybe_submit_header( 'Strict-Transport-Security', false ) ) {
254 return;
255 }
256 $model = $this->get_model();
257 // header is ignored by the browser when your site is accessed using HTTP.
258 if ( true === $model->sh_strict_transport ) {
259 $headers = 'Strict-Transport-Security:';
260 $default_max_age = 604800;
261 if ( isset( $model->hsts_cache_duration ) && '' !== $model->hsts_cache_duration ) {
262 $arr = $this->time_in_seconds();
263 // set default for a week, so RIPs wont waring weak header.
264 $seconds = $arr[ $model->hsts_cache_duration ] ?? $default_max_age;
265 if ( ! is_null( $seconds ) ) {
266 $headers .= ' max-age=' . $seconds;
267 }
268 } else {
269 // set default for a week.
270 $headers .= ' max-age=' . $default_max_age;
271 }
272
273 if ( '1' === (string) $model->include_subdomain ) {
274 $headers .= ' ; includeSubDomains';
275 }
276 if ( '1' === (string) $model->hsts_preload ) {
277 $headers .= ' ; preload';
278 }
279
280 header( $headers );
281 }
282 }
283
284 /**
285 * Retrieves the title of the Policy.
286 *
287 * @return string The title of the Policy.
288 */
289 public function get_title() {
290 return esc_html__( 'Strict Transport', 'defender-security' );
291 }
292 }
293