Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions src/class-status-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, string>
*/
protected function get_raw_request_headers(): array {
return function_exists( 'getallheaders' ) ? (array) getallheaders() : array();
}

/**
Expand Down
81 changes: 81 additions & 0 deletions tests/StatusHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string|null>
*/
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();
}
Expand Down Expand Up @@ -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<string, string> $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<string, string>
*/
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;
}
Comment thread
Copilot marked this conversation as resolved.

// 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;

Expand Down
Loading