'security_headers', 'title' => 'Add security headers', 'desc' => 'Send 3 security headers, plus optional HSTS, Permissions-Policy and COOP', 'icon' => 'lock', 'risk' => 'recommended', 'level' => 'advanced', 'audience' => 'developer', 'priority' => 10, 'benefit' => 'Adds standard browser protections to front-end pages: X-Frame-Options stops other sites from showing your pages in a frame (clickjacking), X-Content-Type-Options stops browsers from guessing file types, and Referrer-Policy limits the address details sent to other sites. Three optional headers add more, with the trade-offs explained below.', 'help' => 'Always sends X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN and Referrer-Policy: strict-origin-when-cross-origin on front-end pages (not in the admin or on the login page). It does not send X-XSS-Protection or a Content-Security-Policy. The three optional headers are off by default because each can be hard to undo or can break something: HSTS makes browsers insist on HTTPS for 180 days, Permissions-Policy turns off camera, microphone and geolocation for the whole site, and COOP can interfere with some cross-site sign-in popups.', 'before' => 'WordPress sends none of these headers on front-end pages (your host may add some)', 'after' => 'Three security headers on every front-end page, plus any optional ones you turn on.', 'extras' => array( array( 'key' => 'sec_hsts_enable', 'type' => 'bool', 'label' => 'Enable HSTS (max-age 180 days, includeSubDomains)', 'default' => 0, 'desc' => 'Only turn this on when HTTPS works permanently on this domain and every subdomain. Browsers that receive this header refuse plain HTTP for 180 days, and you cannot take that back early for browsers that already saw it. The header is only sent on HTTPS requests.', ), array( 'key' => 'sec_permissions_policy_enable', 'type' => 'bool', 'label' => 'Enable Permissions-Policy (camera / microphone / geolocation off)', 'default' => 0, 'desc' => 'Sends Permissions-Policy: camera=(), microphone=(), geolocation=(). This blocks those features on your pages and in every iframe they embed, for example a map that asks for location or a video-call widget. Leave it off if any part of the site needs them.', ), array( 'key' => 'sec_coop_enable', 'type' => 'bool', 'label' => 'Enable Cross-Origin-Opener-Policy (same-origin-allow-popups)', 'default' => 0, 'desc' => 'Sends Cross-Origin-Opener-Policy: same-origin-allow-popups. Pages on other sites that open yours in a new window lose their connection to it, while popups your site opens usually keep working, including most sign-in popups. Turn it off if a social login stops working.', ), ), 'detect' => function () { return array( 'detected' => true, 'message' => 'Security headers not configured', 'safe' => true, 'savings' => 'Security', ); }, 'callback' => function () { add_action( 'send_headers', function () { if ( is_admin() ) { return; } header( 'X-Content-Type-Options: nosniff' ); header( 'X-Frame-Options: SAMEORIGIN' ); header( 'Referrer-Policy: strict-origin-when-cross-origin' ); // Guard HSTS behind is_ssl() — sending Strict-Transport-Security // over plain HTTP has no effect, but guarding keeps dev sites // reachable if somebody toggles HSTS on before TLS is ready. if ( ovkit_get_setting( 'sec_hsts_enable', 0 ) && is_ssl() ) { header( 'Strict-Transport-Security: max-age=15552000; includeSubDomains' ); } if ( ovkit_get_setting( 'sec_permissions_policy_enable', 0 ) ) { header( 'Permissions-Policy: camera=(), microphone=(), geolocation=()' ); } if ( ovkit_get_setting( 'sec_coop_enable', 0 ) ) { header( 'Cross-Origin-Opener-Policy: same-origin-allow-popups' ); } } ); }, );