From c6186d961c2cf1c033a93f8b934e4d600852c0ae Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:00:57 -0500 Subject: [PATCH 01/13] ENG-5386: Test that a valid OnePress one-click token gets past Basic Auth The WP Admin button sends the user to wp-login.php?mpcp_token=..., which OnePress validates on plugins_loaded priority 10 -- after Basic Auth has already challenged on priority 1 and exited. These checks pin the waiver to a token OnePress would accept, and fail closed on every other shape. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/hook-registration-test.php | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index 12ab148..106f6e5 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -38,6 +38,18 @@ function add_filter( $hook, $callback, $priority = 10, $accepted_args = 1 ) { $GLOBALS['hooks'][] = array( 'hook' => $hook, 'callback' => $callback, 'priority' => $priority ); } +$GLOBALS['user_meta'] = array(); +$GLOBALS['deleted_user_meta'] = array(); + +function get_user_meta( $user_id, $key = '', $single = false ) { + return $GLOBALS['user_meta'][ $user_id ][ $key ] ?? ''; +} + +function delete_user_meta( $user_id, $meta_key, $meta_value = '' ) { + $GLOBALS['deleted_user_meta'][] = array( $user_id, $meta_key ); + return true; +} + require __DIR__ . '/../pressable-basic-authentication.php'; $failures = array(); @@ -308,6 +320,119 @@ function skips_auth_for( $uri, $script_name = '/index.php' ) { check( false === skips_auth_for( '/' ), 'an ordinary request is still gated' ); +const ONEPRESS_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'; +const ONEPRESS_SECRET = '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'; + +/** + * Builds an mpcp_token the way MPCP's UpdateMpcpAuthToken does: URL-safe base64, + * unpadded, of "---". + * + * @param int|string $user_id WordPress user id segment. + * @param string $suffix Appended to the payload, to build malformed tokens. + * @return string + */ +function mpcp_token( $user_id = 7, $suffix = '' ) { + $payload = $user_id . '-' . ONEPRESS_SECRET . '-1755013-' . md5( ONEPRESS_UA ) . $suffix; + + return rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' ); +} + +/** + * Whether should_skip_auth() waives Basic Authentication for a OnePress one-click + * login request, with every input the check reads set for the duration of the call. + * + * @param array $request Overrides for: token (null for absent), user_id (whose meta + * is stored), meta (the stored mpcp_auth_token), user_agent, + * pagenow. + * @return bool + */ +function skips_auth_for_onepress( array $request = array() ) { + static $plugin = null; + + if ( null === $plugin ) { + $plugin = new Pressable_Basic_Auth(); + } + + $request += array( + 'token' => mpcp_token(), + 'user_id' => 7, + 'meta' => array( 'value' => md5( ONEPRESS_SECRET ), 'exp' => time() + 30 ), + 'user_agent' => ONEPRESS_UA, + 'pagenow' => 'wp-login.php', + ); + + $original_server = $_SERVER; + $original_get = $_GET; + $original_pagenow = $GLOBALS['pagenow'] ?? null; + + $_SERVER['REQUEST_URI'] = '/wp-login.php'; + $_SERVER['SCRIPT_NAME'] = '/wp-login.php'; + $_SERVER['HTTP_USER_AGENT'] = $request['user_agent']; + $_GET = null === $request['token'] ? array() : array( 'mpcp_token' => $request['token'] ); + $GLOBALS['pagenow'] = $request['pagenow']; + $GLOBALS['user_meta'] = array( $request['user_id'] => array( 'mpcp_auth_token' => $request['meta'] ) ); + + try { + $method = new ReflectionMethod( 'Pressable_Basic_Auth', 'should_skip_auth' ); + $method->setAccessible( true ); + + return (bool) $method->invoke( $plugin ); + } finally { + $_SERVER = $original_server; + $_GET = $original_get; + $GLOBALS['pagenow'] = $original_pagenow; + $GLOBALS['user_meta'] = array(); + } +} + +// Without OnePress loaded nothing would consume the token, so waiving the challenge +// would only expose the wp-login.php password form with no Basic Auth in front of it. +// Asserted before the stand-in class below is declared, which cannot be undone. +check( false === skips_auth_for_onepress(), 'a valid one-click token does not waive auth when OnePress is not active' ); + +if ( ! class_exists( 'Pressable_OnePress_Login_Plugin' ) ) { + final class Pressable_OnePress_Login_Plugin {} +} + +// The MPCP WP Admin button redirects to wp-login.php?mpcp_token=…, which OnePress +// validates on `plugins_loaded` priority 10 -- after this plugin's priority-1 challenge +// has already ended the request. A valid token must therefore get through here. +check( true === skips_auth_for_onepress(), 'a valid, unexpired one-click token on wp-login.php waives auth' ); + +// MPCP omits base64 padding. User id 7 gives a payload needing one `=`; 77 needs none. +check( + true === skips_auth_for_onepress( array( 'token' => mpcp_token( 77 ), 'user_id' => 77 ) ), + 'a one-click token whose encoding has no padding to omit also waives auth' +); + +$GLOBALS['deleted_user_meta'] = array(); +skips_auth_for_onepress(); +check( + array() === $GLOBALS['deleted_user_meta'], + 'validating the token does not consume it -- OnePress deletes it when it logs the user in' +); + +foreach ( array( + 'no token' => array( 'token' => null ), + 'an empty token' => array( 'token' => '' ), + 'a token passed as an array' => array( 'token' => array( mpcp_token() ) ), + 'a page other than wp-login.php' => array( 'pagenow' => 'index.php' ), + 'an expired token' => array( 'meta' => array( 'value' => md5( ONEPRESS_SECRET ), 'exp' => time() - 1 ) ), + 'a wrong secret' => array( 'meta' => array( 'value' => md5( 'other' ), 'exp' => time() + 30 ) ), + 'a different user agent' => array( 'user_agent' => 'curl/8.4.0' ), + 'no stored token' => array( 'meta' => '' ), + 'a stored token missing exp' => array( 'meta' => array( 'value' => md5( ONEPRESS_SECRET ) ) ), + 'a stored token missing value' => array( 'meta' => array( 'exp' => time() + 30 ) ), + 'a non-array stored token' => array( 'meta' => md5( ONEPRESS_SECRET ) ), + 'a token for another user' => array( 'user_id' => 8 ), + 'a token that is not base64' => array( 'token' => '!!!not*base64!!!' ), + 'a token with too few parts' => array( 'token' => rtrim( strtr( base64_encode( '7-' . ONEPRESS_SECRET . '-1755013' ), '+/', '-_' ), '=' ) ), + 'a token with extra parts' => array( 'token' => mpcp_token( 7, '-x' ) ), + 'a non-numeric user id' => array( 'token' => mpcp_token( 'admin' ), 'user_id' => 'admin' ), +) as $description => $request ) { + check( false === skips_auth_for_onepress( $request ), "a one-click request with $description does not waive auth" ); +} + // is_ajax_request() is tested directly rather than through skip_request(): this file runs // under the CLI SAPI, so is_cli_request() (a sibling arm of skip_request()) is always true // here and would mask everything else. From 8de497c9917ae131abd5c541acc26c52a8dbee62 Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:04:47 -0500 Subject: [PATCH 02/13] ENG-5386: Let a valid OnePress one-click login through Basic Auth The MyPressable WP Admin button redirects to wp-login.php?mpcp_token=..., which Pressable OnePress Login validates on plugins_loaded priority 10. Basic Auth challenges on priority 1 and exits, so the token was never seen and the button always ended in a credential prompt. should_skip_auth() now waives the challenge only for a wp-login.php request whose token OnePress will accept: stored token present and unexpired, same user agent, matching secret, and OnePress loaded. The check is read-only, so OnePress still consumes the token and a replayed URL is challenged. Superglobals are read raw because wp_magic_quotes() has not run yet at plugins_loaded. Ships as 1.0.6. Co-Authored-By: Claude Opus 5.5 (1M context) --- pressable-basic-authentication.php | 83 +++++++++++++++++++++++++++++- readme.txt | 10 +++- tests/hook-registration-test.php | 3 +- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/pressable-basic-authentication.php b/pressable-basic-authentication.php index 30c783e..a1dcc2c 100644 --- a/pressable-basic-authentication.php +++ b/pressable-basic-authentication.php @@ -8,7 +8,7 @@ /* Plugin Name: Hosting Basic Authentication Description: Forces all users to authenticate using Basic Authentication before accessing any page. -Version: 1.0.5 +Version: 1.0.6 License: GPL2 Text Domain: hosting-basic-authentication */ @@ -269,9 +269,90 @@ private function should_skip_auth() { return true; } + if ($this->is_valid_onepress_login_request()) { + return true; + } + return false; } + /** + * Whether this is a MyPressable WP Admin one-click login carrying a valid token. + * + * MyPressable sends the user to `wp-login.php?mpcp_token=…`, and the Pressable + * OnePress Login plugin validates that token on `plugins_loaded` priority 10. This + * plugin challenges on priority 1 and exits, so without this waiver the token is + * never seen and the one-click login can only ever end in a credential prompt. + * + * The waiver repeats OnePress's own checks -- stored token present, unexpired, + * same user agent, matching secret -- so it admits exactly the requests OnePress + * will then log in, and nothing else reaches wp-login.php unchallenged. It is + * read-only: OnePress deletes the stored token as it consumes it, which keeps the + * token single-use, so a replayed URL fails here and is challenged as usual. + * + * OnePress must be loaded, or nothing would consume the token and the waiver + * would only expose the password form. Every active plugin file is included before + * `plugins_loaded` fires, so the class check does not depend on load order. + * + * `$_GET` and `$_SERVER` are read raw on purpose: wp_magic_quotes() only runs after + * `plugins_loaded`, so nothing has been slashed yet and wp_unslash() would corrupt + * a value rather than restore it. That is also how OnePress reads them. + * + * @return bool + */ + private function is_valid_onepress_login_request() { + if ( ! class_exists( 'Pressable_OnePress_Login_Plugin', false ) ) { + return false; + } + + if ( 'wp-login.php' !== ( $GLOBALS['pagenow'] ?? '' ) ) { + return false; + } + + $encoded = $_GET['mpcp_token'] ?? null; + + if ( ! is_string( $encoded ) || '' === $encoded ) { + return false; + } + + // MyPressable encodes URL-safe and unpadded; strict mode rejects anything else + // instead of silently discarding the characters it does not recognise. + $decoded = base64_decode( strtr( $encoded, '-_', '+/' ), true ); + + if ( false === $decoded ) { + return false; + } + + // "---"; none of the parts can + // contain a hyphen, so any other count is not a token MyPressable minted. + $parts = explode( '-', $decoded ); + + if ( 4 !== count( $parts ) || 1 !== preg_match( '/\A[0-9]+\z/', $parts[0] ) ) { + return false; + } + + list( $user_id, $secret, , $user_agent_hash ) = $parts; + + $stored = get_user_meta( (int) $user_id, 'mpcp_auth_token', true ); + + if ( ! is_array( $stored ) || ! isset( $stored['value'], $stored['exp'] ) + || ! is_string( $stored['value'] ) || ! is_numeric( $stored['exp'] ) ) { + return false; + } + + if ( (int) $stored['exp'] < time() ) { + return false; + } + + $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; + + if ( ! hash_equals( md5( $user_agent ), $user_agent_hash ) ) { + return false; + } + + return hash_equals( $stored['value'], md5( $secret ) ); + } + /** * Whether a path prefix names a script the server would execute. * diff --git a/readme.txt b/readme.txt index 144252a..7e314bd 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: pressable, basic auth, authentication, security Requires at least: 6.7 Tested up to: 7.1 Requires PHP: 8.1 -Stable tag: 1.0.5 +Stable tag: 1.0.6 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -44,6 +44,14 @@ No manual installation is necessary.​ == Changelog == += 1.0.6 = +* Fixed: the WP Admin button in MyPressable (Pressable OnePress Login) ended in a + Basic Authentication prompt instead of signing the user in. A one-click login + request now passes Basic Authentication only when it carries a token the + OnePress plugin will accept -- present, unexpired, issued to the same browser, + and matching the stored secret -- and only while that plugin is active. Any + other request to wp-login.php is still challenged. + = 1.0.5 = * Fixed: Basic Authentication could be bypassed entirely on any URL, with no credentials, by making a request to a gated page resemble one of the endpoints diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index 106f6e5..52bcfe8 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -428,7 +428,8 @@ final class Pressable_OnePress_Login_Plugin {} 'a token that is not base64' => array( 'token' => '!!!not*base64!!!' ), 'a token with too few parts' => array( 'token' => rtrim( strtr( base64_encode( '7-' . ONEPRESS_SECRET . '-1755013' ), '+/', '-_' ), '=' ) ), 'a token with extra parts' => array( 'token' => mpcp_token( 7, '-x' ) ), - 'a non-numeric user id' => array( 'token' => mpcp_token( 'admin' ), 'user_id' => 'admin' ), + // (int) '7abc' is 7, so this reaches user 7's real token unless the id is rejected first. + 'a non-numeric user id' => array( 'token' => mpcp_token( '7abc' ) ), ) as $description => $request ) { check( false === skips_auth_for_onepress( $request ), "a one-click request with $description does not waive auth" ); } From d8af4f28736c51285f476616383ba18eb0fd6407 Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:26:04 -0500 Subject: [PATCH 03/13] ENG-5386: Test the one-click waiver's input bounds and OnePress readiness Covers the Codex pre-PR findings: an overlong token, a non-string user agent, WordPress installing (when OnePress registers no handler), and a token in its final second, which OnePress still accepts. Any PHP warning or notice now fails the run, so a coerced input cannot pass by failing closed by coincidence. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/hook-registration-test.php | 45 +++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index 52bcfe8..52f0f7b 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -28,6 +28,16 @@ define( 'ABSPATH', __DIR__ ); +// Any warning or notice the plugin raises is a failure: one that coerces an +// unexpected input type can otherwise fail closed by coincidence and pass. +$GLOBALS['php_errors'] = array(); +set_error_handler( + function ( $errno, $errstr, $errfile, $errline ) { + $GLOBALS['php_errors'][] = basename( $errfile ) . ":$errline $errstr"; + return true; + } +); + $GLOBALS['hooks'] = array(); function add_action( $hook, $callback, $priority = 10, $accepted_args = 1 ) { @@ -50,6 +60,10 @@ function delete_user_meta( $user_id, $meta_key, $meta_value = '' ) { return true; } +function wp_installing() { + return ! empty( $GLOBALS['wp_installing'] ); +} + require __DIR__ . '/../pressable-basic-authentication.php'; $failures = array(); @@ -343,7 +357,7 @@ function mpcp_token( $user_id = 7, $suffix = '' ) { * * @param array $request Overrides for: token (null for absent), user_id (whose meta * is stored), meta (the stored mpcp_auth_token), user_agent, - * pagenow. + * pagenow, installing (wp_installing()). * @return bool */ function skips_auth_for_onepress( array $request = array() ) { @@ -359,8 +373,11 @@ function skips_auth_for_onepress( array $request = array() ) { 'meta' => array( 'value' => md5( ONEPRESS_SECRET ), 'exp' => time() + 30 ), 'user_agent' => ONEPRESS_UA, 'pagenow' => 'wp-login.php', + 'installing' => false, ); + $GLOBALS['wp_installing'] = $request['installing']; + $original_server = $_SERVER; $original_get = $_GET; $original_pagenow = $GLOBALS['pagenow'] ?? null; @@ -380,8 +397,9 @@ function skips_auth_for_onepress( array $request = array() ) { } finally { $_SERVER = $original_server; $_GET = $original_get; - $GLOBALS['pagenow'] = $original_pagenow; - $GLOBALS['user_meta'] = array(); + $GLOBALS['pagenow'] = $original_pagenow; + $GLOBALS['user_meta'] = array(); + $GLOBALS['wp_installing'] = false; } } @@ -430,10 +448,26 @@ final class Pressable_OnePress_Login_Plugin {} 'a token with extra parts' => array( 'token' => mpcp_token( 7, '-x' ) ), // (int) '7abc' is 7, so this reaches user 7's real token unless the id is rejected first. 'a non-numeric user id' => array( 'token' => mpcp_token( '7abc' ) ), + // Leading zeros keep the id numeric and (int) 7, so only the length cap rejects this. + 'an overlong token' => array( 'token' => mpcp_token( str_repeat( '0', 300 ) . '7' ) ), + // OnePress hashes the raw header, so a value it cannot hash must not be coerced here. + 'a non-string user agent' => array( 'user_agent' => array( ONEPRESS_UA ) ), + // OnePress registers no login handler while WordPress is installing. + 'WordPress installing' => array( 'installing' => true ), ) as $description => $request ) { check( false === skips_auth_for_onepress( $request ), "a one-click request with $description does not waive auth" ); } +// OnePress rejects only `exp < time()`, so a token in its final second is still live. +// Run in the first half of a second so time() cannot tick between setup and check. +while ( fmod( microtime( true ), 1 ) > 0.5 ) { + usleep( 10000 ); +} +check( + true === skips_auth_for_onepress( array( 'meta' => array( 'value' => md5( ONEPRESS_SECRET ), 'exp' => time() ) ) ), + 'a one-click token expiring this second still waives auth, as OnePress accepts it' +); + // is_ajax_request() is tested directly rather than through skip_request(): this file runs // under the CLI SAPI, so is_cli_request() (a sibling arm of skip_request()) is always true // here and would mask everything else. @@ -478,6 +512,11 @@ function is_ajax_for( $x_requested_with ) { define( 'DOING_AJAX', true ); check( true === is_ajax_for( null ), 'a genuine DOING_AJAX request is still AJAX (bypasses)' ); +check( + array() === $GLOBALS['php_errors'], + 'no check raised a PHP warning or notice' . ( $GLOBALS['php_errors'] ? ': ' . implode( '; ', $GLOBALS['php_errors'] ) : '' ) +); + echo "\n"; if ( $failures ) { From 8cb2edb8c40e7f3d4bff7fb55cb5573e62a37c69 Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:26:54 -0500 Subject: [PATCH 04/13] ENG-5386: Bound the one-click token and mirror OnePress's readiness guard From the Codex pre-PR review: - Cap mpcp_token at 256 characters before decoding or splitting it; a genuine token is about 160. - Refuse a non-string user agent instead of casting it, which turned an array into the literal "Array" (with a warning) and could not match what OnePress hashes. - Refuse while wp_installing(), when OnePress registers no login handler and nothing would consume the token. Its cron, AJAX and WP-CLI arms are already covered by skip_request(). Co-Authored-By: Claude Opus 5.5 (1M context) --- pressable-basic-authentication.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pressable-basic-authentication.php b/pressable-basic-authentication.php index a1dcc2c..1495c49 100644 --- a/pressable-basic-authentication.php +++ b/pressable-basic-authentication.php @@ -290,8 +290,11 @@ private function should_skip_auth() { * read-only: OnePress deletes the stored token as it consumes it, which keeps the * token single-use, so a replayed URL fails here and is challenged as usual. * - * OnePress must be loaded, or nothing would consume the token and the waiver - * would only expose the password form. Every active plugin file is included before + * OnePress must also be ready to handle the request, or nothing would consume the + * token and the waiver would only expose the password form: loaded, and not + * stood down by its own readiness guard. That guard also refuses cron, AJAX and + * WP-CLI, which skip_request() has already let through before this runs, so only + * wp_installing() is repeated here. Every active plugin file is included before * `plugins_loaded` fires, so the class check does not depend on load order. * * `$_GET` and `$_SERVER` are read raw on purpose: wp_magic_quotes() only runs after @@ -301,7 +304,7 @@ private function should_skip_auth() { * @return bool */ private function is_valid_onepress_login_request() { - if ( ! class_exists( 'Pressable_OnePress_Login_Plugin', false ) ) { + if ( ! class_exists( 'Pressable_OnePress_Login_Plugin', false ) || wp_installing() ) { return false; } @@ -311,7 +314,9 @@ private function is_valid_onepress_login_request() { $encoded = $_GET['mpcp_token'] ?? null; - if ( ! is_string( $encoded ) || '' === $encoded ) { + // A genuine token encodes about 120 bytes to about 160 characters. The cap bounds + // the decoding and splitting below, which run before any authentication. + if ( ! is_string( $encoded ) || '' === $encoded || strlen( $encoded ) > 256 ) { return false; } @@ -344,9 +349,9 @@ private function is_valid_onepress_login_request() { return false; } - $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; + $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null; - if ( ! hash_equals( md5( $user_agent ), $user_agent_hash ) ) { + if ( ! is_string( $user_agent ) || ! hash_equals( md5( $user_agent ), $user_agent_hash ) ) { return false; } From 718a07120b4dae474b95a21b894898a2414c6eab Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:28:40 -0500 Subject: [PATCH 05/13] ENG-5386: Test that the waiver stands down whenever OnePress does OnePress's readiness guard reads the filterable wp_doing_ajax() and wp_doing_cron(), not the DOING_* constants skip_request() checks, so a filter can stand OnePress down while the waiver still admits the request. Covers both filters and WP-CLI. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/hook-registration-test.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index 52f0f7b..a3267ed 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -64,6 +64,15 @@ function wp_installing() { return ! empty( $GLOBALS['wp_installing'] ); } +// Filterable in WordPress, so either can be true without DOING_AJAX / DOING_CRON. +function wp_doing_ajax() { + return ! empty( $GLOBALS['wp_doing_ajax'] ); +} + +function wp_doing_cron() { + return ! empty( $GLOBALS['wp_doing_cron'] ); +} + require __DIR__ . '/../pressable-basic-authentication.php'; $failures = array(); @@ -357,7 +366,8 @@ function mpcp_token( $user_id = 7, $suffix = '' ) { * * @param array $request Overrides for: token (null for absent), user_id (whose meta * is stored), meta (the stored mpcp_auth_token), user_agent, - * pagenow, installing (wp_installing()). + * pagenow, installing, doing_ajax, doing_cron (the wp_*() + * results). * @return bool */ function skips_auth_for_onepress( array $request = array() ) { @@ -374,9 +384,13 @@ function skips_auth_for_onepress( array $request = array() ) { 'user_agent' => ONEPRESS_UA, 'pagenow' => 'wp-login.php', 'installing' => false, + 'doing_ajax' => false, + 'doing_cron' => false, ); $GLOBALS['wp_installing'] = $request['installing']; + $GLOBALS['wp_doing_ajax'] = $request['doing_ajax']; + $GLOBALS['wp_doing_cron'] = $request['doing_cron']; $original_server = $_SERVER; $original_get = $_GET; @@ -400,6 +414,8 @@ function skips_auth_for_onepress( array $request = array() ) { $GLOBALS['pagenow'] = $original_pagenow; $GLOBALS['user_meta'] = array(); $GLOBALS['wp_installing'] = false; + $GLOBALS['wp_doing_ajax'] = false; + $GLOBALS['wp_doing_cron'] = false; } } @@ -454,6 +470,10 @@ final class Pressable_OnePress_Login_Plugin {} 'a non-string user agent' => array( 'user_agent' => array( ONEPRESS_UA ) ), // OnePress registers no login handler while WordPress is installing. 'WordPress installing' => array( 'installing' => true ), + // wp_doing_ajax() and wp_doing_cron() are filterable, so either can stand OnePress + // down while the DOING_* constants skip_request() checks are still unset. + 'wp_doing_ajax() filtered true' => array( 'doing_ajax' => true ), + 'wp_doing_cron() filtered true' => array( 'doing_cron' => true ), ) as $description => $request ) { check( false === skips_auth_for_onepress( $request ), "a one-click request with $description does not waive auth" ); } @@ -512,6 +532,10 @@ function is_ajax_for( $x_requested_with ) { define( 'DOING_AJAX', true ); check( true === is_ajax_for( null ), 'a genuine DOING_AJAX request is still AJAX (bypasses)' ); +// OnePress registers no login handler under WP-CLI. Asserted last for the same reason. +define( 'WP_CLI', true ); +check( false === skips_auth_for_onepress(), 'a one-click request under WP-CLI does not waive auth' ); + check( array() === $GLOBALS['php_errors'], 'no check raised a PHP warning or notice' . ( $GLOBALS['php_errors'] ? ': ' . implode( '; ', $GLOBALS['php_errors'] ) : '' ) From 290c6014e6d93c13599b763fe315a30ac1b1eba1 Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:28:57 -0500 Subject: [PATCH 06/13] ENG-5386: Mirror OnePress's readiness guard in full The waiver repeated only wp_installing(), relying on skip_request() for cron, AJAX and WP-CLI. skip_request() reads the DOING_* constants, but OnePress reads the filterable wp_doing_ajax() and wp_doing_cron(), so a filter could stand OnePress down while the waiver still admitted the request. Repeat the whole guard and correct the comment that claimed skip_request() covered it. Raised by the cold review of the Codex fixes. Co-Authored-By: Claude Opus 5.5 (1M context) --- pressable-basic-authentication.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pressable-basic-authentication.php b/pressable-basic-authentication.php index 1495c49..27d230a 100644 --- a/pressable-basic-authentication.php +++ b/pressable-basic-authentication.php @@ -291,11 +291,12 @@ private function should_skip_auth() { * token single-use, so a replayed URL fails here and is challenged as usual. * * OnePress must also be ready to handle the request, or nothing would consume the - * token and the waiver would only expose the password form: loaded, and not - * stood down by its own readiness guard. That guard also refuses cron, AJAX and - * WP-CLI, which skip_request() has already let through before this runs, so only - * wp_installing() is repeated here. Every active plugin file is included before - * `plugins_loaded` fires, so the class check does not depend on load order. + * token and the waiver would only expose the password form: loaded, and not stood + * down by its own readiness guard, which is repeated here in full. skip_request() + * does not make it redundant: it reads the DOING_AJAX and DOING_CRON constants, + * while OnePress reads wp_doing_ajax() and wp_doing_cron(), which are filterable. + * Every active plugin file is included before `plugins_loaded` fires, so the class + * check does not depend on load order. * * `$_GET` and `$_SERVER` are read raw on purpose: wp_magic_quotes() only runs after * `plugins_loaded`, so nothing has been slashed yet and wp_unslash() would corrupt @@ -304,7 +305,11 @@ private function should_skip_auth() { * @return bool */ private function is_valid_onepress_login_request() { - if ( ! class_exists( 'Pressable_OnePress_Login_Plugin', false ) || wp_installing() ) { + if ( ! class_exists( 'Pressable_OnePress_Login_Plugin', false ) ) { + return false; + } + + if ( wp_installing() || wp_doing_cron() || wp_doing_ajax() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { return false; } From 5bb08fb39983d39b504b425edc39ee247fb53c9d Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:33:54 -0500 Subject: [PATCH 07/13] ENG-5386: Document the one-click waiver and the test file's real scope From the Codex confirmation pass: README's exception list named only AJAX, cron and CLI, omitting XML-RPC, the excluded REST namespaces and now the OnePress one-click waiver; readme.txt's feature list did not mention the waiver; and the test file's header still claimed it asserts hook wiring rather than request behaviour, though it exercises both. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 +- readme.txt | 1 + tests/hook-registration-test.php | 11 +++++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b71d51d..0c3fa30 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This plugin implements server-level Basic Authentication for your WordPress site - Sites that need an additional layer of security before the WordPress login Key features: -- Forces Basic Authentication for all requests (except AJAX, CRON, and CLI) +- Forces Basic Authentication for all requests, except: WordPress AJAX, cron and WP-CLI; XML-RPC; the Jetpack, `wp/v2` and `wp/v3` REST namespaces; and a MyPressable WP Admin one-click login carrying a valid, unexpired Pressable OnePress Login token while that plugin is active - Integrates with WordPress user database for authentication - Handles logout functionality properly - Prevents caching of authentication requests diff --git a/readme.txt b/readme.txt index 7e314bd..6efb269 100644 --- a/readme.txt +++ b/readme.txt @@ -17,6 +17,7 @@ The Pressable Basic Authentication plugin enforces HTTP Basic Authentication acr * Enforces HTTP Basic Authentication on all front-end and back-end pages. * Installed on sites to restrict public access. * Allows super administrators to bypass authentication for seamless management. +* Lets the MyPressable WP Admin button sign in: a request carrying a valid, unexpired Pressable OnePress Login token passes, while that plugin is active. * Integrates with WordPress's authentication system for user verification. * Provides a mechanism to log out of Basic Authentication sessions.​ diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index a3267ed..bf54740 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -1,6 +1,8 @@ Date: Thu, 8 Oct 2026 09:35:11 -0500 Subject: [PATCH 08/13] ENG-5386: Complete the README exception list and correct the test file's scope notes README omitted the logged-in-user waiver and implied the REST namespaces are waived however spelled (only /wp-json/ paths are). The test header omitted the source-text checks and the logout guard, and skips_auth_for() still cited parse_url(). Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 +- tests/hook-registration-test.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0c3fa30..752e8c8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This plugin implements server-level Basic Authentication for your WordPress site - Sites that need an additional layer of security before the WordPress login Key features: -- Forces Basic Authentication for all requests, except: WordPress AJAX, cron and WP-CLI; XML-RPC; the Jetpack, `wp/v2` and `wp/v3` REST namespaces; and a MyPressable WP Admin one-click login carrying a valid, unexpired Pressable OnePress Login token while that plugin is active +- Forces Basic Authentication for all requests, except: WordPress AJAX, cron and WP-CLI; XML-RPC; the Jetpack, `wp/v2` and `wp/v3` REST namespaces requested under `/wp-json/`; a MyPressable WP Admin one-click login carrying a valid, unexpired Pressable OnePress Login token while that plugin is active; and any request from a user already logged in to WordPress - Integrates with WordPress user database for authentication - Handles logout functionality properly - Prevents caching of authentication requests diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index bf54740..7854e18 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -1,8 +1,8 @@ Date: Thu, 8 Oct 2026 10:24:31 -0500 Subject: [PATCH 09/13] ENG-5386: Use a neutral site id in the test tokens The fixture token's site id segment was a real customer's site id. The value is never asserted on; a 7-digit placeholder keeps every payload's length, and so the base64 padding cases, unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/hook-registration-test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index 7854e18..3ed4e48 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -358,7 +358,7 @@ function skips_auth_for( $uri, $script_name = '/index.php' ) { * @return string */ function mpcp_token( $user_id = 7, $suffix = '' ) { - $payload = $user_id . '-' . ONEPRESS_SECRET . '-1755013-' . md5( ONEPRESS_UA ) . $suffix; + $payload = $user_id . '-' . ONEPRESS_SECRET . '-1000001-' . md5( ONEPRESS_UA ) . $suffix; return rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' ); } @@ -463,7 +463,7 @@ final class Pressable_OnePress_Login_Plugin {} 'a non-array stored token' => array( 'meta' => md5( ONEPRESS_SECRET ) ), 'a token for another user' => array( 'user_id' => 8 ), 'a token that is not base64' => array( 'token' => '!!!not*base64!!!' ), - 'a token with too few parts' => array( 'token' => rtrim( strtr( base64_encode( '7-' . ONEPRESS_SECRET . '-1755013' ), '+/', '-_' ), '=' ) ), + 'a token with too few parts' => array( 'token' => rtrim( strtr( base64_encode( '7-' . ONEPRESS_SECRET . '-1000001' ), '+/', '-_' ), '=' ) ), 'a token with extra parts' => array( 'token' => mpcp_token( 7, '-x' ) ), // (int) '7abc' is 7, so this reaches user 7's real token unless the id is rejected first. 'a non-numeric user id' => array( 'token' => mpcp_token( '7abc' ) ), From d73127258ee1108617ca12252e30db7c450cc8f2 Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:23:55 -0500 Subject: [PATCH 10/13] ENG-5386: Test that the waiver needs OnePress to see the same token OnePress registers its handler on, and reads, $_REQUEST, which at plugins_loaded is PHP's own, built from request_order. With an order that leaves out G, OnePress never sees a query-string token, so a waiver keyed on $_GET alone would let wp-login.php through with nothing to consume the token. Raised by Rogelio in review. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/hook-registration-test.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/hook-registration-test.php b/tests/hook-registration-test.php index 3ed4e48..863e74b 100644 --- a/tests/hook-registration-test.php +++ b/tests/hook-registration-test.php @@ -370,7 +370,8 @@ function mpcp_token( $user_id = 7, $suffix = '' ) { * @param array $request Overrides for: token (null for absent), user_id (whose meta * is stored), meta (the stored mpcp_auth_token), user_agent, * pagenow, installing, doing_ajax, doing_cron (the wp_*() - * results). + * results), request_token ($_REQUEST's mpcp_token, null for + * absent; defaults to the $_GET token, as request_order GP gives). * @return bool */ function skips_auth_for_onepress( array $request = array() ) { @@ -391,18 +392,24 @@ function skips_auth_for_onepress( array $request = array() ) { 'doing_cron' => false, ); + if ( ! array_key_exists( 'request_token', $request ) ) { + $request['request_token'] = $request['token']; + } + $GLOBALS['wp_installing'] = $request['installing']; $GLOBALS['wp_doing_ajax'] = $request['doing_ajax']; $GLOBALS['wp_doing_cron'] = $request['doing_cron']; $original_server = $_SERVER; $original_get = $_GET; + $original_request = $_REQUEST; $original_pagenow = $GLOBALS['pagenow'] ?? null; $_SERVER['REQUEST_URI'] = '/wp-login.php'; $_SERVER['SCRIPT_NAME'] = '/wp-login.php'; $_SERVER['HTTP_USER_AGENT'] = $request['user_agent']; $_GET = null === $request['token'] ? array() : array( 'mpcp_token' => $request['token'] ); + $_REQUEST = null === $request['request_token'] ? array() : array( 'mpcp_token' => $request['request_token'] ); $GLOBALS['pagenow'] = $request['pagenow']; $GLOBALS['user_meta'] = array( $request['user_id'] => array( 'mpcp_auth_token' => $request['meta'] ) ); @@ -414,6 +421,7 @@ function skips_auth_for_onepress( array $request = array() ) { } finally { $_SERVER = $original_server; $_GET = $original_get; + $_REQUEST = $original_request; $GLOBALS['pagenow'] = $original_pagenow; $GLOBALS['user_meta'] = array(); $GLOBALS['wp_installing'] = false; @@ -473,6 +481,10 @@ final class Pressable_OnePress_Login_Plugin {} 'a non-string user agent' => array( 'user_agent' => array( ONEPRESS_UA ) ), // OnePress registers no login handler while WordPress is installing. 'WordPress installing' => array( 'installing' => true ), + // OnePress registers on, and reads, $_REQUEST. With a request_order that leaves out + // G it never sees a query-string token, and a body value can differ from the query one. + 'the token absent from $_REQUEST' => array( 'request_token' => null ), + 'a different $_REQUEST token' => array( 'request_token' => mpcp_token( 8 ) ), // wp_doing_ajax() and wp_doing_cron() are filterable, so either can stand OnePress // down while the DOING_* constants skip_request() checks are still unset. 'wp_doing_ajax() filtered true' => array( 'doing_ajax' => true ), From ac519a76bb0a9523b1fef40ed36dcac3a967028b Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:24:31 -0500 Subject: [PATCH 11/13] ENG-5386: Waive only when OnePress will see the same token; correct overstated claims From Rogelio's review: - Require $_REQUEST['mpcp_token'] to be the query-string token. OnePress registers on and reads $_REQUEST, built from request_order at plugins_loaded; without the check, a request_order that leaves out G (or a body value overriding the query one) let wp-login.php through with OnePress never handling the request. - The docblock no longer claims the waiver admits "exactly" what OnePress logs in (it is deliberately narrower), and the strict-base64 comment no longer claims a format check the code does not make. - readme.txt: the binding is the User-Agent, not the browser, and other wp-login.php requests are challenged unless an existing exemption applies. Co-Authored-By: Claude Opus 5.5 (1M context) --- pressable-basic-authentication.php | 30 +++++++++++++++++++++--------- readme.txt | 7 ++++--- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/pressable-basic-authentication.php b/pressable-basic-authentication.php index 27d230a..326c10e 100644 --- a/pressable-basic-authentication.php +++ b/pressable-basic-authentication.php @@ -285,10 +285,16 @@ private function should_skip_auth() { * never seen and the one-click login can only ever end in a credential prompt. * * The waiver repeats OnePress's own checks -- stored token present, unexpired, - * same user agent, matching secret -- so it admits exactly the requests OnePress - * will then log in, and nothing else reaches wp-login.php unchallenged. It is - * read-only: OnePress deletes the stored token as it consumes it, which keeps the - * token single-use, so a replayed URL fails here and is challenged as usual. + * same user agent, matching secret -- and is deliberately narrower than OnePress + * (the token must arrive in the query string, in exactly four parts), so it admits + * only requests OnePress will then log in. It is read-only: OnePress deletes the + * stored token as it consumes it, which keeps the token single-use, so a replayed + * URL fails here and is challenged as usual. + * + * OnePress registers on, and reads, `$_REQUEST`, which at `plugins_loaded` is still + * PHP's own, built from `request_order`. The query-string token must also be the + * one `$_REQUEST` holds, or a `request_order` without G (or a body value overriding + * the query one) would let wp-login.php through with OnePress never handling it. * * OnePress must also be ready to handle the request, or nothing would consume the * token and the waiver would only expose the password form: loaded, and not stood @@ -298,9 +304,10 @@ private function should_skip_auth() { * Every active plugin file is included before `plugins_loaded` fires, so the class * check does not depend on load order. * - * `$_GET` and `$_SERVER` are read raw on purpose: wp_magic_quotes() only runs after - * `plugins_loaded`, so nothing has been slashed yet and wp_unslash() would corrupt - * a value rather than restore it. That is also how OnePress reads them. + * `$_GET`, `$_REQUEST` and `$_SERVER` are read raw on purpose: wp_magic_quotes() + * only runs after `plugins_loaded`, so nothing has been slashed yet and + * wp_unslash() would corrupt a value rather than restore it. That is also how + * OnePress reads them. * * @return bool */ @@ -325,8 +332,13 @@ private function is_valid_onepress_login_request() { return false; } - // MyPressable encodes URL-safe and unpadded; strict mode rejects anything else - // instead of silently discarding the characters it does not recognise. + if ( ( $_REQUEST['mpcp_token'] ?? null ) !== $encoded ) { + return false; + } + + // MyPressable encodes URL-safe and unpadded, so map it to the standard alphabet. + // Strict mode returns false on any character outside that alphabet instead of + // silently discarding it; it still accepts padding and skips whitespace. $decoded = base64_decode( strtr( $encoded, '-_', '+/' ), true ); if ( false === $decoded ) { diff --git a/readme.txt b/readme.txt index 6efb269..681014b 100644 --- a/readme.txt +++ b/readme.txt @@ -49,9 +49,10 @@ No manual installation is necessary.​ * Fixed: the WP Admin button in MyPressable (Pressable OnePress Login) ended in a Basic Authentication prompt instead of signing the user in. A one-click login request now passes Basic Authentication only when it carries a token the - OnePress plugin will accept -- present, unexpired, issued to the same browser, - and matching the stored secret -- and only while that plugin is active. Any - other request to wp-login.php is still challenged. + OnePress plugin will accept -- present, unexpired, presented with the same + User-Agent it was issued to, and matching the stored secret -- and only while + that plugin is active. Any other request to wp-login.php is still challenged + unless an existing exemption applies, such as an existing WordPress session. = 1.0.5 = * Fixed: Basic Authentication could be bypassed entirely on any URL, with no From 3955e105e914c3233c0fc2cdae14c9b1a34b67ac Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:25:54 -0500 Subject: [PATCH 12/13] ENG-5386: Test the OnePress waiver through real plugins_loaded dispatch The hook test calls should_skip_auth() directly, because under the CLI SAPI is_cli_request() waives every request; nothing exercised the path plugins_loaded -> init() -> skip_request() -> challenge. This serves a router from PHP's built-in web server (the cli-server SAPI), fires plugins_loaded with a OnePress stand-in, and asserts a valid token reaches OnePress unchallenged while no-token, wrong-UA and non-login requests get the 401. It fails against the released 1.0.5. Raised by CodeRabbit. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/main.yml | 3 + .github/workflows/test.yml | 3 + tests/fixtures/onepress-dispatch-router.php | 108 +++++++++++++++ tests/onepress-dispatch-test.php | 141 ++++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 tests/fixtures/onepress-dispatch-router.php create mode 100644 tests/onepress-dispatch-test.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab05024..198e8d1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,6 +54,9 @@ jobs: - name: Hook registration regression test run: php tests/hook-registration-test.php + - name: OnePress waiver dispatch test + run: php tests/onepress-dispatch-test.php + build: name: Package Release Project runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1442198..d140e42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,3 +45,6 @@ jobs: - name: Hook registration regression test run: php tests/hook-registration-test.php + + - name: OnePress waiver dispatch test + run: php tests/onepress-dispatch-test.php diff --git a/tests/fixtures/onepress-dispatch-router.php b/tests/fixtures/onepress-dispatch-router.php new file mode 100644 index 0000000..7eb8d82 --- /dev/null +++ b/tests/fixtures/onepress-dispatch-router.php @@ -0,0 +1,108 @@ + md5( DISPATCH_SECRET ), 'exp' => time() + 30 ); +} + +/** + * Stand-in for Pressable OnePress Login: registers on the same hook and priority, under + * the same condition, and reports that it was reached instead of logging anyone in. + */ +final class Pressable_OnePress_Login_Plugin { + public function __construct() { + if ( 'wp-login.php' === $GLOBALS['pagenow'] && isset( $_REQUEST['mpcp_token'] ) ) { + add_action( 'plugins_loaded', array( $this, 'handle_server_login_request' ) ); + } + } + + public function handle_server_login_request() { + echo 'ONEPRESS-HANDLED'; + exit; + } +} + +require __DIR__ . '/../../pressable-basic-authentication.php'; +new Pressable_OnePress_Login_Plugin(); + +do_action( 'plugins_loaded' ); + +echo 'NOT-CHALLENGED-NOR-HANDLED'; diff --git a/tests/onepress-dispatch-test.php b/tests/onepress-dispatch-test.php new file mode 100644 index 0000000..df2dae6 --- /dev/null +++ b/tests/onepress-dispatch-test.php @@ -0,0 +1,141 @@ + array( 'file', '/dev/null', 'w' ), 2 => array( 'file', '/dev/null', 'w' ) ), + $pipes +); + +register_shutdown_function( + function () use ( $server ) { + proc_terminate( $server ); + proc_close( $server ); + } +); + +$ready = false; + +for ( $attempt = 0; $attempt < 50 && ! $ready; $attempt++ ) { + $connection = @fsockopen( '127.0.0.1', $port, $errno, $errstr, 0.1 ); + + if ( $connection ) { + fclose( $connection ); + $ready = true; + } else { + usleep( 100000 ); + } +} + +if ( ! $ready ) { + echo "FAIL the built-in server did not start on port $port\n"; + exit( 1 ); +} + +/** + * Requests a path from the router and returns its status, challenge header and body. + * + * @param string $path Request path and query. + * @param string $user_agent User-Agent header to send. + * @return array{status: int, challenged: bool, body: string} + */ +function dispatch( $path, $user_agent = DISPATCH_UA ) { + global $port; + + $context = stream_context_create( + array( + 'http' => array( + 'header' => "User-Agent: $user_agent\r\n", + 'ignore_errors' => true, + 'timeout' => 10, + ), + ) + ); + + $body = (string) @file_get_contents( "http://127.0.0.1:$port$path", false, $context ); + $headers = $http_response_header ?? array(); + $status = preg_match( '#^HTTP/\S+ (\d{3})#', $headers[0] ?? '', $match ) ? (int) $match[1] : 0; + + return array( + 'status' => $status, + 'challenged' => (bool) preg_grep( '/^WWW-Authenticate: Basic/i', $headers ), + 'body' => $body, + ); +} + +echo "OnePress waiver dispatch (built-in server)\n"; + +$valid = dispatch( '/wp-login.php?mpcp_token=' . dispatch_token() ); +check( + 200 === $valid['status'] && ! $valid['challenged'] && 'ONEPRESS-HANDLED' === $valid['body'], + 'a valid one-click token reaches OnePress through plugins_loaded with no challenge' +); + +foreach ( array( + 'no token' => array( '/wp-login.php', DISPATCH_UA ), + 'a different user agent' => array( '/wp-login.php?mpcp_token=' . dispatch_token(), 'curl/8.4.0' ), + 'a token for another UA' => array( '/wp-login.php?mpcp_token=' . dispatch_token( 'curl/8.4.0' ), DISPATCH_UA ), + 'a page other than wp-login.php' => array( '/?mpcp_token=' . dispatch_token(), DISPATCH_UA ), +) as $description => list( $path, $user_agent ) ) { + $response = dispatch( $path, $user_agent ); + check( + 401 === $response['status'] && $response['challenged'] && 'ONEPRESS-HANDLED' !== $response['body'], + "a request with $description is challenged before OnePress" + ); +} + +echo "\n"; + +if ( $failures ) { + echo count( $failures ) . " failure(s)\n"; + exit( 1 ); +} + +echo "All checks passed\n"; +exit( 0 ); From 8d286f5add1b012fcb009f7f2120700b34094b9f Mon Sep 17 00:00:00 2001 From: loukieluke <84424020+loukieluke@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:47:32 -0500 Subject: [PATCH 13/13] ENG-5386: Correct how the $_REQUEST paragraph describes a body override With the stock request_order, a different POST mpcp_token still lands in $_REQUEST, so OnePress does handle the request; the risk is that it consumes a token the waiver never checked, not that it ignores the request. Only a request_order without G leaves OnePress unregistered. Raised by Rogelio in review. Co-Authored-By: Claude Opus 5.5 (1M context) --- pressable-basic-authentication.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pressable-basic-authentication.php b/pressable-basic-authentication.php index 326c10e..53ec8cb 100644 --- a/pressable-basic-authentication.php +++ b/pressable-basic-authentication.php @@ -293,8 +293,9 @@ private function should_skip_auth() { * * OnePress registers on, and reads, `$_REQUEST`, which at `plugins_loaded` is still * PHP's own, built from `request_order`. The query-string token must also be the - * one `$_REQUEST` holds, or a `request_order` without G (or a body value overriding - * the query one) would let wp-login.php through with OnePress never handling it. + * one `$_REQUEST` holds. Otherwise a `request_order` without G would let wp-login.php + * through with OnePress never handling it, and a body value overriding the query one + * would have OnePress consume a token the waiver never checked. * * OnePress must also be ready to handle the request, or nothing would consume the * token and the waiver would only expose the password form: loaded, and not stood