diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea9aa77..796c4c72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This library adheres to [Semantic Versioning](https://semver.org/) and [Keep a C ### Added +* `login_nonce`: Added a `no-store` header to the wp-login.php page. * `prevent_framing`: Added a feature to prevent framing of the site via the `X-Frame-Options` header. diff --git a/src/alley/wp/alleyvate/features/class-login-nonce.php b/src/alley/wp/alleyvate/features/class-login-nonce.php index ae49a7ea..0e462950 100644 --- a/src/alley/wp/alleyvate/features/class-login-nonce.php +++ b/src/alley/wp/alleyvate/features/class-login-nonce.php @@ -51,6 +51,27 @@ public function boot(): void { add_action( 'login_form_login', [ self::class, 'action__add_nonce_life_filter' ] ); add_action( 'login_head', [ self::class, 'action__add_meta_refresh' ] ); add_action( 'after_setup_theme', [ self::class, 'action__pre_validate_login_nonce' ], 9999 ); + add_filter( 'nocache_headers', [ self::class, 'add_no_store_to_login' ] ); + } + + /** + * Adds the `no-store` flag to the `Cache-Control` headers. + * + * @param array $headers The headers array. + * @return array + */ + public static function add_no_store_to_login( $headers ): array { + if ( ! \is_array( $headers ) ) { + $headers = []; + } + + if ( 'wp-login.php' !== ( $GLOBALS['pagenow'] ?? '' ) ) { + return $headers; + } + + $headers['Cache-Control'] = 'no-cache, must-revalidate, max-age=0, no-store'; + + return $headers; } /** diff --git a/tests/alley/wp/alleyvate/features/test-login-nonce.php b/tests/alley/wp/alleyvate/features/test-login-nonce.php index 2918b50f..e5a27bad 100644 --- a/tests/alley/wp/alleyvate/features/test-login-nonce.php +++ b/tests/alley/wp/alleyvate/features/test-login-nonce.php @@ -144,4 +144,40 @@ public function test_logout_nonce_validates(): void { $this->assertTrue( wp_validate_boolean( wp_verify_nonce( $token, 'log-out' ) ) ); } + + /** + * Verify that the no-store flag is added to the login page. + * + * Note: `wp_get_nocache_headers()` is used by `nocache_headers()` which + * in turn is called on `wp-login.php`. We call it directly here so + * we can assert against an array instead of trying to send headers. + */ + public function test_login_page_cache_is_no_stored() { + global $pagenow; + + $pagenow = 'wp-login.php'; + + $this->feature->boot(); + + $headers = wp_get_nocache_headers(); + + self::assertArrayHasKey( 'Cache-Control', $headers ); + self::assertStringContainsString( 'no-store', $headers['Cache-Control'] ); + } + + /** + * Verify that the no-store flag isn't added to other pages. + */ + public function test_non_login_page_is_stored() { + global $pagenow; + + $pagenow = 'single.php'; // Anything other than wp-login.php. + + $this->feature->boot(); + + $headers = wp_get_nocache_headers(); + + self::assertArrayHasKey( 'Cache-Control', $headers ); + self::assertStringNotContainsString( 'no-store', $headers['Cache-Control'] ); + } }