1package Security::TLSCheck::Checks::Web; 2 3use 5.010; 4 5use Carp; 6use English qw( -no_match_vars ); 7 8use Moose; 9extends 'Security::TLSCheck::Checks'; 10with 'Security::TLSCheck::Checks::Helper::Timing'; 11 12use Log::Log4perl::EasyCatch; 13 14use LWP::UserAgent; 15use HTTP::Status qw(HTTP_OK HTTP_INTERNAL_SERVER_ERROR); 16 17# Preload later required libraries (for parallel fork mode) 18use HTTP::Response; 19use HTTP::Request; 20use LWP::Protocol::https; 21use LWP::Protocol::http; 22use Mozilla::CA; 23use IO::Socket::SSL; 24 25 26use Readonly; 27Readonly my $NOT_FOUND => -1; 28 29 30=head1 NAME 31 32Security::TLSCheck::Checks::Web - (Basic) HTTP and HTTPS Checks 33 34=encoding utf8 35 36=cut 37 38use version; our $VERSION = sprintf "%d", q$Revision: 640 $ =~ /(\d+)/xg; 39 40 41=head1 SYNOPSIS 42 43... 44 45 46=head1 DESCRIPTION 47 48This module checks some (basic) HTTP key figures: 49 50 * HTTP / HTTPS for domain or www domain active; status OK? 51 * HTTP redirects to HTTPS? 52 * redirections 53 * simple HTTPS Certificate Verification (via LWP with help from Mozilla::CA) 54 55 56For simplification of the results, this check first tries to use http://www.domain.tld/ 57and only if this does gives an result only http://domain.tld/ 58 59So we don't have to count two results per domain, only one. 60 61 62=cut 63 64#<<< 65 66{ 67my $key_figures = 68 [ 69 { name => "HTTP active", type => "flag", source => "http_active", description => "Is there a HTTP server on Port 80? (all Status OK)", }, 70 { name => "HTTP OK", type => "flag", source => "http_ok", description => "HTTP-Server returns Status 200 OK", }, 71 { name => "HTTPS active", type => "flag", source => "https_active", description => "Is there a HTTPS server on Port 443? (all states are OK)", }, 72 { name => "HTTPS host verified", type => "flag", source => "https_host_verified", description => "HTTPS is active and host matches", }, 73 { name => "HTTPS cert verified", type => "flag", source => "https_cert_verified", description => "HTTPS is active and certificate is verified against Mozilla::CA", }, 74 { name => "HTTPS wrong host, cert OK", type => "flag", source => "https_cert_ok_host_not", description => "HTTPS is active but host does not match", }, 75 { name => "HTTPS all verified", type => "flag", source => "https_all_verified", description => "HTTPS is active, host matches and certificate is verified against Mozilla::CA", }, 76 { name => "HTTPS OK", type => "flag", source => "https_ok", description => "HTTPS returns Status 200 OK (certificate/host not checked)", }, 77 { name => "HTTPS all verified and OK", type => "flag", source => "https_all_ok", description => "HTTPS returns Status 200 OK (certificate and host are checked)", }, 78 { name => "Redirect to HTTPS", type => "flag", source => "redirects_to_https", description => "HTTP redirects to HTTPS", }, 79 { name => "Redirect to HTTP", type => "flag", source => "redirects_to_http", description => "HTTPS redirects to HTTP", }, 80 { name => "Supports HSTS", type => "flag", source => "hsts_max_age", description => "Supports HTTP Strict Transport Security", }, 81 { name => "HSTS max age", type => "int", source => "hsts_max_age", description => "How long browsers should cache HTTP Strict Transport Security", }, 82 { name => "Disables HSTS", type => "flag", source => "disables_hsts", description => "HTTP Strict Transport Security is disabled by server", }, 83 { name => "Used cipher suite", type => "group", source => "cipher_suite", description => "The cipher suite, selected by the server", }, 84 { name => "Certificate issuer", type => "group", source => "cert_issuer", description => "Issuer of the certificate", }, 85 { name => "Certificate Let's Encrypt", type => "flag", source => "cert_letsencrypt", description => "Issuer of the certificate is Let's Encrypt", }, 86 { name => "Certificate self-signed", type => "flag", source => "cert_selfsigned", description => "Certificate is self-signed", }, 87 { name => "Cert self-signed, host OK", type => "flag", source => "cert_selfsigned_hostok", description => "Certificate is self-signed and host matches", }, 88 { name => "Server full string", type => "group", source => "server", description => "The full server string (HTTP)", }, 89 { name => "Server name", type => "group", source => "server_name", description => "The server name string", }, 90 { name => "Server name/major version", type => "group", source => "server_major_version", description => "The server name and major version", }, 91 { name => "Supports HPKP", type => "flag", source => "has_hpkp", description => "Server has a public key pinng header", }, 92 { name => "Supports HPKP report", type => "flag", source => "has_hpkp_report", description => "Server has a report-only public key pinng header", }, 93 ]; 94 95 96has '+key_figures' => ( default => sub {return $key_figures} ); 97} 98 99has '+description' => ( default => "(Basic) HTTP and HTTPS Checks" ); 100 101has _ua => ( is => "ro", isa => "LWP::UserAgent", lazy_build => 1, ); 102has _http_response => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, ); 103has _https_response => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, ); 104has _https_response_hostcheck => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, ); 105has _https_response_certcheck => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, ); 106has _https_response_nocheck => ( is => "ro", isa => "HTTP::Response", lazy_build => 1, ); 107 108 109#>>> 110 111 112sub _build__ua 113 { 114 my $self = shift; 115 return LWP::UserAgent->new( timeout => $self->timeout, agent => $self->user_agent_name, ); 116 } 117 118sub _build__http_response 119 { 120 my $self = shift; 121 return $self->_do_request("http"); 122 } 123 124sub _build__https_response 125 { 126 my $self = shift; 127 return $self->_do_request("https"); 128 } 129 130sub _build__https_response_hostcheck 131 { 132 my $self = shift; 133 return $self->_do_request( "https", { verify_hostname => 1, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE } ); 134 } 135 136sub _build__https_response_certcheck 137 { 138 my $self = shift; 139 return $self->_do_request( "https", { verify_hostname => 0, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER } ); 140 } 141 142sub _build__https_response_nocheck 143 { 144 my $self = shift; 145 return $self->_do_request( "https", { verify_hostname => 0, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE } ); 146 } 147 148sub _do_request 149 { 150 my $self = shift; 151 my $protocol = shift; 152 my $ssl_opts = shift // { verify_hostname => 1, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER }; 153 154 my $ua = $self->_ua; 155 $ua->ssl_opts(%$ssl_opts); 156 157 # If check with www. if not successful, try without www 158 # if this is also not successful, use the www result! 159 my $response = $ua->get( "$protocol://" . $self->www ); 160 unless ( $response->is_success ) 161 { 162 my $domain_response = $ua->get( "$protocol://" . $self->domain ); 163 $response = $domain_response if $domain_response->is_success; 164 } 165 166 return $response; 167 } 168 169 170 171=head1 METHODS 172 173=head2 http_active, https_active, https_all_verified, https_host_verified, https_cert_verified, https_cert_ok_host_not 174 175Checks, if there is something on port 80/443 ... 176 177Fails when Status is 500 and there is a "Client-Warning" header with "Internal response" 178 179 * https_active is true, if there is HTTPS, certificate verify failed, but there is https. 180 * https_all_verified is only true, if certificate verification is OK and host matches. 181 * https_host_verified is true, if the SSL host matches, but cert is not checked 182 * https_cert_verified is true, if there is a valid certificate (Mozilla::CA), hostname not checked 183 * https_cert_ok_host_not is true, if there is a valid certificate, but hostname does NOT match 184 185=cut 186 187#sub _has_https 188# { 189# my $self = shift; 190# return 1 if _valid_response( $self->_https_response_nocheck ); 191# return; 192# } 193 194sub _valid_response 195 { 196 my $response = shift; 197 198 return 1 unless $response->code == HTTP_INTERNAL_SERVER_ERROR; 199 return 1 unless defined $response->header("Client-Warning"); 200 return 1 unless $response->header("Client-Warning") eq "Internal response"; 201 return 0; 202 } 203 204sub http_active 205 { 206 my $self = shift; 207 return _valid_response( $self->_http_response ); 208 } 209 210sub https_active 211 { 212 my $self = shift; 213 214 return 1 if _valid_response( $self->_https_response_nocheck ); 215 216 # Old method; still active because tests can't check ssl_opts 217 # return 1 if $self->https_all_verified; 218 # return 1 if index( $self->_https_response->status_line, "certificate verify failed" ) != $NOT_FOUND; 219 return 0; 220 } 221 222 223 224sub https_all_verified 225 { 226 my $self = shift; 227 return unless $self->https_active; # when no HTTPS active, don't check more HTTPS 228 return _valid_response( $self->_https_response ); 229 } 230 231sub https_host_verified 232 { 233 my $self = shift; 234 return unless $self->https_active; # when no HTTPS active, don't check more HTTPS 235 return _valid_response( $self->_https_response_hostcheck ); 236 } 237 238sub https_cert_verified 239 { 240 my $self = shift; 241 return unless $self->https_active; # when no HTTPS active, don't check more HTTPS 242 return _valid_response( $self->_https_response_certcheck ); 243 } 244 245sub https_cert_ok_host_not 246 { 247 my $self = shift; 248 return unless $self->https_active; # when no HTTPS active, don't check more HTTPS 249 return 1 if $self->https_cert_verified and not $self->https_host_verified; 250 return 0; 251 } 252 253=head2 http_ok, https_ok, https_all_ok 254 255Returns true if HTTP request was sucessful and no error (status Code 2xx) 256 257=cut 258 259sub http_ok 260 { 261 my $self = shift; 262 return $self->_http_response->is_success; 263 } 264 265sub https_ok 266 { 267 my $self = shift; 268 return $self->_https_response_nocheck->is_success; 269 } 270 271sub https_all_ok 272 { 273 my $self = shift; 274 return unless $self->https_active; # when no HTTPS active, don't check more HTTPS 275 return $self->_https_response->is_success; 276 } 277 278=head2 redirects_to_https 279 280Returns true, all HTTP Requests (on the start page) are redirected to HTTPS 281 282=cut 283 284sub redirects_to_https 285 { 286 my $self = shift; 287 288 #use Data::Dumper; 289 290 # DEBUG "Redir to HTTPSSSS? Domain: " . $self->domain; 291 # DEBUG Dumper( $self->_http_response ); 292 # 293 # 294 295 # look at the last request in the HTTP request chain; if there 296 # was a redirect to HTTPS, then there is the URI ... 297 return 1 if $self->_http_response->request->uri =~ m{^https}x; 298 return 0; 299 } 300 301=head2 redirects_to_http 302 303Returns true, if HTTPS Requests (on the startpage) are redirected to HTTP 304 305Checked for all HTTPS conections, including invalid Certs. 306 307 308=cut 309 310sub redirects_to_http 311 { 312 my $self = shift; 313 314 return unless $self->https_active; # when no HTTPS active, don't check more HTTPS 315 316 317 # use Data::Dumper; 318 319 # DEBUG "Redir to HTTP? Domain: " . $self->domain; 320 # DEBUG Dumper( $self->_https_response ); 321 # 322 323 324 # look at the last request in the HTTPS request chain; if there 325 # was a redirect to HTTP, then there is the URI ... 326 return 1 if $self->_https_response_nocheck->request->uri =~ m{^http:}x; 327 return 0; 328 } 329 330 331 332=head2 hsts_max_age 333 334Returns the max-age value of the Strict-Transport-Security header. 335 336Checked for all certs (also when invalid). 337 338Returns undef, if there is none. 339 340 341RFC says: The max-age directive value can optionally be quoted: 342 343 Strict-Transport-Security: max-age="31536000" 344 345=cut 346 347sub hsts_max_age 348 { 349 my $self = shift; 350 351 my @hsts = $self->_https_response_nocheck->header("Strict-Transport-Security"); 352 return unless @hsts; 353 354 my %hsts = map { _split_hsts($ARG) } map { split( m{\s*;\s*}x, $ARG ) } @hsts; 355 356 DEBUG "Probably parsing error: found a HSTS header, but no max_age for @hsts at " . $self->www unless defined $hsts{"max-age"}; 357 358 # remove all non-numbers, because some hsts headers are broken. 359 ( my $filtered_hsts = $hsts{"max-age"} ) =~ s{\D}{}g; 360 361 return $filtered_hsts; 362 363 } 364 365sub _split_hsts 366 { 367 my $param = shift; 368 369 my ( $key, $value ) = $param =~ m{ ^ ([-\w]+) \s* (?: = \s* (.*) )? }x; 370 $value //= ""; 371 372 # strip sourrounding " and ' 373 $value =~ s{ ^ (["']) (.*) \1 $}{$2}gx; 374 375 return ( lc($key), $value ); 376 377 } 378 379 380 381=head2 disables_hsts 382 383Does the site resets HTTP Strict Transport Security? 384 385This is the case, when the max_age is set to 0. 386 387=cut 388 389sub disables_hsts 390 { 391 my $self = shift; 392 my $max_age = $self->hsts_max_age // return 0; 393 394 return 1 if $max_age == 0; 395 return 0; 396 } 397 398 399=head2 cipher_suite 400 401Extracts the used cipher_suite from the HTTP-Headers (Client-SSL-Cipher) 402 403Checked for all HTTPS connections, also checked when invalid certificate 404 405=cut 406 407sub cipher_suite 408 { 409 my $self = shift; 410 return $self->_https_response_nocheck->header("Client-SSL-Cipher"); 411 } 412 413=head2 cert_issuer 414 415Extracts certifivate issuer from the HTTP-Headers (Client-SSL-Cert-Issuer) 416 417ONLY FOR VALID CERTS! 418 419=cut 420 421sub cert_issuer 422 { 423 my $self = shift; 424 return unless $self->https_cert_verified; # only remember issuer, when it's a valid CA 425 return $self->_https_response->header("Client-SSL-Cert-Issuer"); 426 } 427 428=head2 ->cert_letsencrypt 429 430Checks, if the cert is signed by Let's Encrypt 431 432=cut 433 434sub cert_letsencrypt 435 { 436 my $self = shift; 437 my $cert_issuer = $self->cert_issuer // return; 438 return index( $cert_issuer, "Let's Encrypt" ) > -1; 439 } 440 441 442=head2 ->cert_selfsigned 443 444Checks, if the cert is selfsigned 445 446=cut 447 448sub cert_selfsigned 449 { 450 my $self = shift; 451 my $cert_issuer = $self->_https_response->header("Client-SSL-Cert-Issuer") // return; 452 my $cert_subject = $self->_https_response->header("Client-SSL-Cert-Subject") // return; 453 return $cert_subject eq $cert_issuer; 454 } 455 456=head2 ->cert_selfsigned_hostok 457 458Checks, if the cert is selfsigned AND the hostname matches 459 460=cut 461 462sub cert_selfsigned_hostok 463 { 464 my $self = shift; 465 return ( $self->https_host_verified and $self->cert_selfsigned ); 466 } 467 468 469=head2 server 470 471Extracts server string from Server header. 472 473=cut 474 475sub server 476 { 477 my $self = shift; 478 return $self->_http_response->header("Server"); 479 } 480 481=head2 server_name 482 483Server name, without other informations (Version, modules, ...) 484 485=cut 486 487sub server_name 488 { 489 my $self = shift; 490 return _get_server_name( $self->server ); 491 } 492 493sub _get_server_name 494 { 495 my $server = shift // return; 496 497 my ($name) = $server =~ m{ ^ ([^/]*) }x; 498 return $name if length($name) < 20; 499 500 $name =~ s{ ( [^\w\s].* ) }{}xg; 501 502 return $name; 503 504 } 505 506 507=head2 server_major_version 508 509Server name and major version, without other informations (patchlevel, modules, ...) 510 511=cut 512 513sub server_major_version 514 { 515 my $self = shift; 516 517 return _get_server_major_version( $self->server ); 518 519 } 520 521sub _get_server_major_version 522 { 523 my $server = shift // return; 524 525 my ($name_version) = $server =~ m{ ( [-\w\s]+ (?: / \d+ (?:[.]\d+) )? ) }x; 526 527 return $name_version // _get_server_name($server); 528 529 } 530 531 532=head2 has_hpkp 533 534Supports HTTP Public Key pinning (Public-Key-Pins Header). 535 536Checked for all HTTPS conections, including invalid Certs. 537 538=cut 539 540sub has_hpkp 541 { 542 my $self = shift; 543 return 1 if $self->_https_response_nocheck->header("Public-Key-Pins"); 544 } 545 546 547=head2 has_hpkp_report 548 549Supports HTTP Public Key pinning, report only (Public-Key-Pins-Report-Only Header). 550 551Checked for all HTTPS conections, including invalid Certs. 552 553 554=cut 555 556sub has_hpkp_report 557 { 558 my $self = shift; 559 return 1 if $self->_https_response_nocheck->header("Public-Key-Pins-Report-Only"); 560 } 561 562 563 564__PACKAGE__->meta->make_immutable; 565 5661; 567 568