| 1 |
<?php |
| 2 |
/** |
| 3 |
* Author: Hoang Ngo |
| 4 |
*/ |
| 5 |
|
| 6 |
namespace WP_Defender\Component\Security_Headers; |
| 7 |
|
| 8 |
use WP_Defender\Component\Security_Header; |
| 9 |
|
| 10 |
class Sh_Strict_Transport extends Security_Header { |
| 11 |
static $rule_slug = 'sh_strict_transport'; |
| 12 |
|
| 13 |
/** |
| 14 |
* Get time in seconds |
| 15 |
* |
| 16 |
* @return array |
| 17 |
*/ |
| 18 |
private function time_in_seconds() { |
| 19 |
return array( |
| 20 |
'1 hour' => 1 * 3600, |
| 21 |
'24 hours' => 86400, |
| 22 |
'7 days' => 7 * 86400, |
| 23 |
'30 days' => 30 * 86400, |
| 24 |
'3 months' => ( 3 * 30 + 1 ) * 86400, |
| 25 |
'6 months' => ( 6 * 30 + 3 ) * 86400, |
| 26 |
'1 year' => 365 * 86400, |
| 27 |
'2 years' => 365 * 2 * 86400, |
| 28 |
); |
| 29 |
} |
| 30 |
|
| 31 |
/** |
| 32 |
* Check HTTPS |
| 33 |
* |
| 34 |
* @return bool |
| 35 |
*/ |
| 36 |
private function is_https() { |
| 37 |
return isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']; |
| 38 |
} |
| 39 |
|
| 40 |
public function check() { |
| 41 |
$model = $this->get_model(); |
| 42 |
|
| 43 |
if ( ! $model->sh_strict_transport ) { |
| 44 |
return false; |
| 45 |
} |
| 46 |
//'max-age' directive is required |
| 47 |
if ( ! empty( $model->hsts_cache_duration ) ) { |
| 48 |
return true; |
| 49 |
} |
| 50 |
$headers = $this->head_request( network_site_url(), self::$rule_slug ); |
| 51 |
if ( is_wp_error( $headers ) ) { |
| 52 |
$this->log( sprintf( 'Self ping error: %s', $headers->get_error_message() ) ); |
| 53 |
|
| 54 |
return false; |
| 55 |
} |
| 56 |
|
| 57 |
if ( isset( $headers['strict-transport-security'] ) ) { |
| 58 |
$hsts_cache_duration = ''; |
| 59 |
$hsts_preload = 0; |
| 60 |
$include_subdomain = 0; |
| 61 |
$header_sts = is_array( $headers['strict-transport-security'] ) |
| 62 |
? $headers['strict-transport-security'][0] |
| 63 |
: $headers['strict-transport-security']; |
| 64 |
|
| 65 |
$content = explode( ';', $header_sts ); |
| 66 |
foreach ( $content as $line ) { |
| 67 |
if ( stristr( $line, 'max-age' ) ) { |
| 68 |
$value = explode( '=', $line ); |
| 69 |
$arr = $this->time_in_seconds(); |
| 70 |
$seconds = isset( $value[1] ) ? (int) $value[1] : 0; |
| 71 |
$closest = null; |
| 72 |
$key = null; |
| 73 |
foreach ( $arr as $k => $item ) { |
| 74 |
if ( is_null( $closest ) || ( ! is_null( $closest ) && ( abs( $seconds - $closest ) > abs( $item - $seconds ) ) ) ) { |
| 75 |
$closest = $item; |
| 76 |
$key = $k; |
| 77 |
} |
| 78 |
} |
| 79 |
$hsts_cache_duration = $key; |
| 80 |
} elseif ( stristr( $line, 'preload' ) ) { |
| 81 |
$hsts_preload = 1; |
| 82 |
} elseif ( stristr( $line, 'includeSubDomains' ) ) { |
| 83 |
$include_subdomain = 1; |
| 84 |
} |
| 85 |
} |
| 86 |
|
| 87 |
if ( ( '' !== $hsts_cache_duration ) |
| 88 |
|| ( 0 !== $hsts_preload ) |
| 89 |
|| ( 0 !== $include_subdomain ) |
| 90 |
) { |
| 91 |
if ( is_null( $model->hsts_preload ) && $hsts_preload ) { |
| 92 |
$model->hsts_preload = $hsts_preload; |
| 93 |
} |
| 94 |
if ( is_null( $model->include_subdomain ) && $include_subdomain ) { |
| 95 |
$model->include_subdomain = $include_subdomain; |
| 96 |
} |
| 97 |
if ( is_null( $model->hsts_cache_duration ) && $hsts_cache_duration ) { |
| 98 |
$model->hsts_cache_duration = $hsts_cache_duration; |
| 99 |
} |
| 100 |
$model->save(); |
| 101 |
} |
| 102 |
|
| 103 |
return true; |
| 104 |
} |
| 105 |
|
| 106 |
return false; |
| 107 |
} |
| 108 |
|
| 109 |
private function get_domain_suffix( $domain ) { |
| 110 |
require_once dirname( __DIR__ ) . '/public-suffix.php'; |
| 111 |
$tlds = get_public_suffix(); |
| 112 |
//whitelist development |
| 113 |
$tlds['localhost'] = 1; |
| 114 |
$parts = explode( '.', $domain ); |
| 115 |
$parts = array_reverse( $parts ); |
| 116 |
$suffix = ''; |
| 117 |
$list = array(); |
| 118 |
$length = 0; |
| 119 |
foreach ( $parts as $part ) { |
| 120 |
$suffix = rtrim( $part . '.' . $suffix, '.' ); |
| 121 |
$not_allow = '!' . $suffix; |
| 122 |
if ( isset( $tlds[ $not_allow ] ) ) { |
| 123 |
//this wont be here |
| 124 |
continue; |
| 125 |
} |
| 126 |
if ( isset( $tlds[ $suffix ] ) ) { |
| 127 |
if ( $length > strlen( $suffix ) ) { |
| 128 |
//put at last |
| 129 |
$list[] = $suffix; |
| 130 |
} else { |
| 131 |
array_unshift( $list, $suffix ); |
| 132 |
} |
| 133 |
} |
| 134 |
} |
| 135 |
if ( empty( $list ) ) { |
| 136 |
return false; |
| 137 |
} |
| 138 |
|
| 139 |
return $list[0]; |
| 140 |
} |
| 141 |
|
| 142 |
public function parse_domain( $domain ) { |
| 143 |
$filter_domain = version_compare( PHP_VERSION, '7.0', '>=' ) ? FILTER_VALIDATE_DOMAIN : FILTER_VALIDATE_URL; |
| 144 |
if ( ! filter_var( $domain, $filter_domain ) ) { |
| 145 |
return false; |
| 146 |
} |
| 147 |
$suffix = $this->get_domain_suffix( $domain ); |
| 148 |
if ( ! $suffix ) { |
| 149 |
return false; |
| 150 |
} |
| 151 |
//exclude 'www.' |
| 152 |
$domain = str_replace( 'www.', '', $domain ); |
| 153 |
$host = parse_url( $domain, PHP_URL_HOST ); |
| 154 |
$host_without_tld = str_replace( $suffix, '', $host ); |
| 155 |
|
| 156 |
$host_without_tld = rtrim( $host_without_tld, '.' ); |
| 157 |
$parts = explode( '.', $host_without_tld ); |
| 158 |
if ( 1 === count( $parts ) ) { |
| 159 |
return array( |
| 160 |
'host' => $host, |
| 161 |
'tld' => $suffix, |
| 162 |
); |
| 163 |
} |
| 164 |
//parse to get the root & subdomain |
| 165 |
$domain = array_pop( $parts ); |
| 166 |
|
| 167 |
return array( |
| 168 |
'host' => $host, |
| 169 |
'tld' => $suffix, |
| 170 |
'subdomain' => str_replace( $domain, '', $host_without_tld ), |
| 171 |
); |
| 172 |
} |
| 173 |
|
| 174 |
public function get_misc_data() { |
| 175 |
$model = $this->get_model(); |
| 176 |
$site_url = network_site_url(); |
| 177 |
$domain_data = $this->parse_domain( $site_url ); |
| 178 |
$allow_subdomain = false; |
| 179 |
|
| 180 |
if ( is_array( $domain_data ) && ! isset( $domain_data['subdomain'] ) ) { |
| 181 |
$allow_subdomain = true; |
| 182 |
} elseif ( ! $domain_data && ! is_multisite() ) { |
| 183 |
//case if a single site installs in a folder, e.g. http://example.com/something/folder/ |
| 184 |
$allow_subdomain = true; |
| 185 |
} elseif ( ! $domain_data && is_multisite() && is_subdomain_install() && is_main_site() ) { |
| 186 |
//case if main site on MU with subdomain install |
| 187 |
$allow_subdomain = true; |
| 188 |
} |
| 189 |
|
| 190 |
return array( |
| 191 |
'intro_text' => esc_html__( '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.', 'wpdef' ), |
| 192 |
'hsts_preload' => $model->hsts_preload ?? 0, |
| 193 |
'include_subdomain' => $model->include_subdomain ?? 0, |
| 194 |
'hsts_cache_duration' => $model->hsts_cache_duration ?? '30 days', |
| 195 |
'allow_subdomain' => $allow_subdomain, |
| 196 |
); |
| 197 |
} |
| 198 |
|
| 199 |
public function add_hooks() { |
| 200 |
add_action( 'send_headers', array( $this, 'append_header' ) ); |
| 201 |
} |
| 202 |
|
| 203 |
public function append_header() { |
| 204 |
if ( headers_sent() ) { |
| 205 |
return; |
| 206 |
} |
| 207 |
|
| 208 |
if ( ! $this->maybe_submit_header( 'Strict-Transport-Security', false ) ) { |
| 209 |
return; |
| 210 |
} |
| 211 |
$model = $this->get_model(); |
| 212 |
//header is ignored by the browser when your site is accessed using HTTP |
| 213 |
if ( true === $model->sh_strict_transport ) { |
| 214 |
$headers = 'Strict-Transport-Security:'; |
| 215 |
$default_max_age = 604800; |
| 216 |
if ( isset( $model->hsts_cache_duration ) && ! empty( $model->hsts_cache_duration ) ) { |
| 217 |
$arr = $this->time_in_seconds(); |
| 218 |
//set default for a week, so RIPs wont waring weak header |
| 219 |
$seconds = $arr[ $model->hsts_cache_duration ] ?? $default_max_age; |
| 220 |
if ( ! is_null( $seconds ) ) { |
| 221 |
$headers .= ' max-age=' . $seconds; |
| 222 |
} |
| 223 |
} else { |
| 224 |
//set default for a week |
| 225 |
$headers .= ' max-age=' . $default_max_age; |
| 226 |
} |
| 227 |
|
| 228 |
if ( '1' === (string) $model->include_subdomain ) { |
| 229 |
$headers .= ' ; includeSubDomains'; |
| 230 |
} |
| 231 |
if ( '1' === (string) $model->hsts_preload ) { |
| 232 |
$headers .= ' ; preload'; |
| 233 |
} |
| 234 |
|
| 235 |
header( $headers ); |
| 236 |
} |
| 237 |
} |
| 238 |
|
| 239 |
/** |
| 240 |
* @return string |
| 241 |
*/ |
| 242 |
public function get_title() { |
| 243 |
return __( 'Strict Transport', 'wpdef' ); |
| 244 |
} |
| 245 |
} |
| 246 |
|