diff --git a/composer.json b/composer.json index 3c5c6a8..c34f995 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=8.1", - "getsupertab/connect-sdk-php": "1.4.0-beta.9" + "getsupertab/connect-sdk-php": "1.4.0-beta.10" }, "require-dev": { "automattic/vipwpcs": "^3.0", diff --git a/composer.lock b/composer.lock index 907fc56..15c1d57 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0797e65a85e4ad5676db4fafb2d27bd8", + "content-hash": "27d73b0be0f2d8d820573210e2a59ce4", "packages": [ { "name": "firebase/php-jwt", @@ -74,16 +74,16 @@ }, { "name": "getsupertab/connect-sdk-php", - "version": "v1.4.0-beta.9", + "version": "v1.4.0-beta.10", "source": { "type": "git", "url": "https://github.com/getsupertab/connect-sdk-php.git", - "reference": "ad5459a7f73d8a50f284b4dee6182b33b96f08eb" + "reference": "5f16f85ea477bab7c1272ed9dde9ba5eb513c6bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsupertab/connect-sdk-php/zipball/ad5459a7f73d8a50f284b4dee6182b33b96f08eb", - "reference": "ad5459a7f73d8a50f284b4dee6182b33b96f08eb", + "url": "https://api.github.com/repos/getsupertab/connect-sdk-php/zipball/5f16f85ea477bab7c1272ed9dde9ba5eb513c6bf", + "reference": "5f16f85ea477bab7c1272ed9dde9ba5eb513c6bf", "shasum": "" }, "require": { @@ -125,9 +125,9 @@ ], "support": { "issues": "https://github.com/getsupertab/connect-sdk-php/issues", - "source": "https://github.com/getsupertab/connect-sdk-php/tree/v1.4.0-beta.9" + "source": "https://github.com/getsupertab/connect-sdk-php/tree/v1.4.0-beta.10" }, - "time": "2026-07-14T08:41:59+00:00" + "time": "2026-07-14T13:11:23+00:00" } ], "packages-dev": [ diff --git a/src/class-status-handler.php b/src/class-status-handler.php index 12d0d1a..fc3711b 100644 --- a/src/class-status-handler.php +++ b/src/class-status-handler.php @@ -20,6 +20,7 @@ use Supertab\Connect\Http\HttpClient; use Supertab\Connect\Http\HttpClientInterface; +use Supertab\Connect\Http\RequestContext; use Supertab\Connect\Jwks\JwksProvider; use Supertab\Connect\Status\StatusChallengeVerifier; use Supertab_Connect\Utils\WP_Transient_Cache; @@ -212,15 +213,27 @@ private function get_verifier(): StatusChallengeVerifier { } /** - * Read the Authorization header, with the common Apache CGI fallback. + * Read the Authorization header. Apache withholds it from the CGI-style + * $_SERVER variables, so fall back to the SAPI's raw request headers via + * the SDK's resolver ($_SERVER wins when both are present). * * @return string */ - private function get_authorization_header(): string { + protected function get_authorization_header(): string { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below via sanitize_text_field()/wp_unslash(). - $header = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; + $header = RequestContext::resolveAuthorizationHeader( $_SERVER, $this->get_raw_request_headers() ) ?? ''; - return sanitize_text_field( wp_unslash( (string) $header ) ); + return sanitize_text_field( wp_unslash( $header ) ); + } + + /** + * Raw request headers as reported by the SAPI. Overridable seam for tests; + * getallheaders() is unavailable under the CLI SAPI. + * + * @return array + */ + protected function get_raw_request_headers(): array { + return function_exists( 'getallheaders' ) ? (array) getallheaders() : array(); } /** diff --git a/tests/StatusHandlerTest.php b/tests/StatusHandlerTest.php index 1d386ca..8056aae 100644 --- a/tests/StatusHandlerTest.php +++ b/tests/StatusHandlerTest.php @@ -20,15 +20,36 @@ class StatusHandlerTest extends TestCase { private HttpClientInterface $http_client; + /** + * Snapshot of the $_SERVER authorization keys, restored in tearDown so + * tests that mutate them cannot leak state (PHPUnit does not back up + * superglobals in this configuration). + * + * @var array + */ + private array $server_auth_snapshot = array(); + protected function setUp(): void { parent::setUp(); wp_stubs_reset(); + foreach ( array( 'HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION' ) as $key ) { + $this->server_auth_snapshot[ $key ] = $_SERVER[ $key ] ?? null; + } + $this->settings = new Settings(); $this->http_client = $this->createMock( HttpClientInterface::class ); } protected function tearDown(): void { + foreach ( $this->server_auth_snapshot as $key => $value ) { + if ( null === $value ) { + unset( $_SERVER[ $key ] ); + } else { + $_SERVER[ $key ] = $value; + } + } + wp_stubs_reset(); parent::tearDown(); } @@ -188,6 +209,66 @@ static function () use ( &$called ): bool { $this->assertFalse( $called, 'Verifier must not run for other paths.' ); } + public function test_authorization_header_falls_back_to_raw_request_headers(): void { + // Apache withholds Authorization from the CGI-style $_SERVER variables; + // getallheaders() still exposes it. Regression test for the live bug + // where every backend challenge probe received the 404 decoy. + unset( $_SERVER['HTTP_AUTHORIZATION'], $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ); + + $handler = $this->create_handler_with_raw_headers( array( 'authorization' => 'Bearer apache-token' ) ); + + $this->assertSame( 'Bearer apache-token', $handler->exposed_authorization_header() ); + } + + public function test_authorization_header_prefers_server_over_raw_request_headers(): void { + $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer from-server'; + + $handler = $this->create_handler_with_raw_headers( array( 'Authorization' => 'Bearer from-raw-headers' ) ); + + $this->assertSame( 'Bearer from-server', $handler->exposed_authorization_header() ); + } + + public function test_authorization_header_empty_when_absent_everywhere(): void { + unset( $_SERVER['HTTP_AUTHORIZATION'], $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ); + + $handler = $this->create_handler_with_raw_headers( array( 'Accept' => 'application/json' ) ); + + $this->assertSame( '', $handler->exposed_authorization_header() ); + } + + /** + * Build a Status_Handler whose raw request headers are injected, with the + * protected header resolution exposed for assertions. + * + * @param array $raw_headers Raw request headers to inject. + */ + private function create_handler_with_raw_headers( array $raw_headers ) { + return new class( $this->settings, 'https://api-connect.sbx.supertab.co', $this->http_client, null, $raw_headers ) extends Status_Handler { + /** + * Raw request headers injected by the test. + * + * @var array + */ + private array $raw_headers; + + // phpcs:ignore Squiz.Commenting.FunctionComment.Missing + public function __construct( Settings $settings, string $api_base_url, HttpClientInterface $http_client, ?\Closure $verify_challenge, array $raw_headers ) { + parent::__construct( $settings, $api_base_url, $http_client, $verify_challenge ); + $this->raw_headers = $raw_headers; + } + + // phpcs:ignore Squiz.Commenting.FunctionComment.Missing + public function exposed_authorization_header(): string { + return $this->get_authorization_header(); + } + + // phpcs:ignore Squiz.Commenting.FunctionComment.Missing + protected function get_raw_request_headers(): array { + return $this->raw_headers; + } + }; + } + public function test_register_hooks_parse_request_before_bot_protection(): void { global $wp_test_actions;