| @@ -309,8 +309,28 @@ | ||
| 309 | 309 | return str_replace( array( '"', "\r", "\n" ), '', (string) $value ); |
| 310 | 310 | } |
| 311 | 311 | |
| 312 | 312 | /** |
| 313 | + * Whether the site address is an https:// one | |
| 314 | + * | |
| 315 | + * Read from the stored address and from nothing else. is_ssl() is false | |
| 316 | + * behind a proxy that terminates TLS and says nothing in a cron run or from | |
| 317 | + * WP-CLI, and home_url() takes its scheme from is_ssl() when the request is | |
| 318 | + * secure (wp-includes/link-template.php:3471-3477 in 7.1.2), so the same site | |
| 319 | + * would write different rules from an https request and from a scheduled | |
| 320 | + * task. Vigilante_Htaccess_Recovery and File | |
| 321 | + * Integrity compare the file with what this class would write now, so the | |
| 322 | + * answer has to be the same wherever it is asked. | |
| 323 | + * | |
| 324 | + * @since 3.0.2 | |
| 325 | + * | |
| 326 | + * @return bool | |
| 327 | + */ | |
| 328 | + private function site_address_is_https() { | |
| 329 | + return 0 === strpos( (string) get_option( 'home' ), 'https://' ); | |
| 330 | + } | |
| 331 | + | |
| 332 | + /** | |
| 313 | 333 | * Generate rules content (without markers) |
| 314 | 334 | * |
| 315 | 335 | * Public since 2.10.0: it is also "what this configuration would write right |
| 316 | 336 | * now", which is what Vigilante_Htaccess_Recovery compares the file against. |
| @@ -414,9 +434,25 @@ | ||
| 414 | 434 | } |
| 415 | 435 | } |
| 416 | 436 | |
| 417 | 437 | if ( ! empty( $csp['directives'] ) && is_array( $csp['directives'] ) ) { |
| 438 | + $site_is_https = $this->site_address_is_https(); | |
| 439 | + | |
| 418 | 440 | foreach ( $csp['directives'] as $directive => $value ) { |
| 441 | + /* | |
| 442 | + * upgrade-insecure-requests tells the browser to ask for every | |
| 443 | + * http:// resource of the page over https. On a site whose | |
| 444 | + * address is http:// nothing answers there, so every | |
| 445 | + * stylesheet, script and image stops loading, and the factory | |
| 446 | + * policy carries it. It is written only when the site address | |
| 447 | + * is https://. The setting itself is left as it is, so the | |
| 448 | + * next rewrite of the block after the address changes writes | |
| 449 | + * it. | |
| 450 | + */ | |
| 451 | + if ( 'upgrade-insecure-requests' === strtolower( (string) $directive ) && ! $site_is_https ) { | |
| 452 | + continue; | |
| 453 | + } | |
| 454 | + | |
| 419 | 455 | // Override frame-ancestors with synchronized value if X-Frame-Options is set |
| 420 | 456 | if ( 'frame-ancestors' === $directive && ! empty( $frame_ancestors_value ) ) { |
| 421 | 457 | $directives[] = $directive . ' ' . $frame_ancestors_value; |
| 422 | 458 | } elseif ( true === $value ) { |
| @@ -515,114 +551,46 @@ | ||
| 515 | 551 | return implode( "\n", $rules ); |
| 516 | 552 | } |
| 517 | 553 | |
| 518 | 554 | /** |
| 519 | - * Get security grade based on enabled headers | |
| 555 | + * Warnings about the Content-Security-Policy as it is configured | |
| 520 | 556 | * |
| 521 | - * @return array | |
| 557 | + * They are about what is saved in the tab, not about what reaches the | |
| 558 | + * browser: that is measured by the checks of Security Check, which is | |
| 559 | + * also what the Test Headers button shows. Until 3.0.4 that button graded | |
| 560 | + * these settings and listed them as enabled headers without asking the | |
| 561 | + * site for anything. | |
| 562 | + * | |
| 563 | + * @since 3.0.5 | |
| 564 | + * | |
| 565 | + * @return string[] | |
| 522 | 566 | */ |
| 523 | - public function get_security_grade() { | |
| 524 | - $score = 0; | |
| 525 | - $enabled = array(); | |
| 526 | - $missing = array(); | |
| 567 | + public function get_csp_warnings() { | |
| 527 | 568 | $warnings = array(); |
| 528 | 569 | |
| 529 | - // X-Frame-Options (15 points) | |
| 530 | - if ( ! empty( $this->options['x_frame_options'] ) ) { | |
| 531 | - $score += 15; | |
| 532 | - $enabled[] = 'X-Frame-Options: ' . $this->options['x_frame_options']; | |
| 533 | - } else { | |
| 534 | - $missing[] = 'X-Frame-Options'; | |
| 570 | + if ( empty( $this->options['csp']['enabled'] ) ) { | |
| 571 | + return $warnings; | |
| 535 | 572 | } |
| 536 | 573 | |
| 537 | - // X-Content-Type-Options (15 points) | |
| 538 | - if ( ! empty( $this->options['x_content_type_options'] ) ) { | |
| 539 | - $score += 15; | |
| 540 | - $enabled[] = 'X-Content-Type-Options: nosniff'; | |
| 541 | - } else { | |
| 542 | - $missing[] = 'X-Content-Type-Options'; | |
| 574 | + if ( ! empty( $this->options['csp']['report_only'] ) ) { | |
| 575 | + $warnings[] = __( 'CSP is in report-only mode: the browser reports what the policy would block, and blocks nothing.', 'vigilante' ); | |
| 576 | + return $warnings; | |
| 543 | 577 | } |
| 544 | 578 | |
| 545 | - // HSTS (20 points) | |
| 546 | - if ( ! empty( $this->options['hsts']['enabled'] ) ) { | |
| 547 | - $hsts = $this->options['hsts']; | |
| 548 | - if ( $hsts['max_age'] >= 31536000 ) { | |
| 549 | - $score += 20; | |
| 550 | - } else { | |
| 551 | - $score += 10; | |
| 552 | - $warnings[] = 'HSTS max-age should be at least 1 year (31536000 seconds)'; | |
| 553 | - } | |
| 554 | - $enabled[] = 'Strict-Transport-Security'; | |
| 555 | - } else { | |
| 556 | - $missing[] = 'Strict-Transport-Security (HSTS)'; | |
| 579 | + if ( $this->is_csp_restrictive() ) { | |
| 580 | + $warnings[] = __( 'Restrictive CSP detected. Admin area is automatically excluded to prevent breaking the dashboard and block editor.', 'vigilante' ); | |
| 557 | 581 | } |
| 558 | 582 | |
| 559 | - // CSP (30 points; absorbs the 10 points freed by retiring the deprecated X-XSS-Protection) | |
| 560 | - if ( ! empty( $this->options['csp']['enabled'] ) ) { | |
| 561 | - if ( empty( $this->options['csp']['report_only'] ) ) { | |
| 562 | - $score += 30; | |
| 563 | - $enabled[] = 'Content-Security-Policy'; | |
| 564 | - | |
| 565 | - // Add warning if CSP is restrictive | |
| 566 | - if ( $this->is_csp_restrictive() ) { | |
| 567 | - $warnings[] = __( 'Restrictive CSP detected. Admin area is automatically excluded to prevent breaking the dashboard and block editor.', 'vigilante' ); | |
| 568 | - } | |
| 569 | - | |
| 570 | - // Check for specific compatibility issues | |
| 571 | - $csp_issues = $this->get_csp_compatibility_issues(); | |
| 572 | - foreach ( $csp_issues as $issue ) { | |
| 573 | - if ( 'high' === $issue['severity'] ) { | |
| 574 | - $warnings[] = sprintf( | |
| 575 | - /* translators: 1: CSP directive name, 2: Issue description */ | |
| 576 | - __( 'CSP %1$s: %2$s', 'vigilante' ), | |
| 577 | - $issue['directive'], | |
| 578 | - $issue['issue'] | |
| 579 | - ); | |
| 580 | - } | |
| 581 | - } | |
| 582 | - } else { | |
| 583 | - $score += 15; | |
| 584 | - $enabled[] = 'Content-Security-Policy-Report-Only'; | |
| 585 | - $warnings[] = 'CSP is in report-only mode (recommended for testing)'; | |
| 583 | + foreach ( $this->get_csp_compatibility_issues() as $issue ) { | |
| 584 | + if ( 'high' === $issue['severity'] ) { | |
| 585 | + $warnings[] = sprintf( | |
| 586 | + /* translators: 1: CSP directive name, 2: Issue description */ | |
| 587 | + __( 'CSP %1$s: %2$s', 'vigilante' ), | |
| 588 | + $issue['directive'], | |
| 589 | + $issue['issue'] | |
| 590 | + ); | |
| 586 | 591 | } |
| 587 | - } else { | |
| 588 | - $missing[] = 'Content-Security-Policy'; | |
| 589 | 592 | } |
| 590 | 593 | |
| 591 | - // Referrer-Policy (10 points) | |
| 592 | - if ( ! empty( $this->options['referrer_policy'] ) ) { | |
| 593 | - $score += 10; | |
| 594 | - $enabled[] = 'Referrer-Policy: ' . $this->options['referrer_policy']; | |
| 595 | - } else { | |
| 596 | - $missing[] = 'Referrer-Policy'; | |
| 597 | - } | |
| 598 | - | |
| 599 | - // Permissions-Policy (10 points) | |
| 600 | - if ( ! empty( $this->options['permissions_policy']['enabled'] ) ) { | |
| 601 | - $score += 10; | |
| 602 | - $enabled[] = 'Permissions-Policy'; | |
| 603 | - } else { | |
| 604 | - $missing[] = 'Permissions-Policy'; | |
| 605 | - } | |
| 606 | - | |
| 607 | - // Calculate grade | |
| 608 | - if ( $score >= 90 ) { | |
| 609 | - $grade = 'A'; | |
| 610 | - } elseif ( $score >= 80 ) { | |
| 611 | - $grade = 'B'; | |
| 612 | - } elseif ( $score >= 70 ) { | |
| 613 | - $grade = 'C'; | |
| 614 | - } elseif ( $score >= 60 ) { | |
| 615 | - $grade = 'D'; | |
| 616 | - } else { | |
| 617 | - $grade = 'F'; | |
| 618 | - } | |
| 619 | - | |
| 620 | - return array( | |
| 621 | - 'grade' => $grade, | |
| 622 | - 'score' => $score, | |
| 623 | - 'headers' => $enabled, | |
| 624 | - 'missing' => $missing, | |
| 625 | - 'warnings' => $warnings, | |
| 626 | - ); | |
| 594 | + return $warnings; | |
| 627 | 595 | } |
| 628 | 596 | } |