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
37 changes: 33 additions & 4 deletions src/Http/RequestContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@ public function __construct(
public ?CdnRequestSignals $cdnSignals = null,
) {}

/**
* Resolve the Authorization header from $_SERVER, falling back to the
* SAPI's raw request headers.
*
* Apache with mod_php withholds Authorization from the CGI-style
* $_SERVER variables (a CGI-era security behavior), while
* getallheaders() still exposes it. Without the fallback, challenge and
* license-token verification silently fail closed on vanilla Apache —
* every bearer looks absent.
*
* @param array<string, mixed> $server Typically $_SERVER
* @param array<string, mixed> $requestHeaders Raw request headers as reported by getallheaders()
*/
public static function resolveAuthorizationHeader(array $server, array $requestHeaders): ?string
{
$auth = $server['HTTP_AUTHORIZATION'] ?? $server['REDIRECT_HTTP_AUTHORIZATION'] ?? null;
if (is_string($auth) && $auth !== '') {
return $auth;
}

foreach ($requestHeaders as $name => $value) {
if (strcasecmp((string) $name, 'Authorization') === 0 && is_string($value) && $value !== '') {
return $value;
}
}

return null;
}

/**
* Build a RequestContext from PHP's $_SERVER superglobal.
*/
Expand All @@ -46,10 +75,10 @@ public static function fromGlobals(): self
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$url = "{$scheme}://{$host}{$uri}";

// PHP strips the Authorization header in some setups; check multiple sources
$authorization = $_SERVER['HTTP_AUTHORIZATION']
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
?? null;
$authorization = self::resolveAuthorizationHeader(
$_SERVER,
function_exists('getallheaders') ? (getallheaders() ?: []) : [],
);
Comment thread
Copilot marked this conversation as resolved.

$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
$accept = $_SERVER['HTTP_ACCEPT'] ?? null;
Expand Down
50 changes: 50 additions & 0 deletions tests/Http/RequestContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,56 @@ public function test_from_globals_populates_method_client_ip_and_request_id(): v
}
}

public function test_resolve_authorization_prefers_server_http_authorization(): void
{
$auth = RequestContext::resolveAuthorizationHeader(
['HTTP_AUTHORIZATION' => 'Bearer from-server'],
['Authorization' => 'Bearer from-headers'],
);

$this->assertSame('Bearer from-server', $auth);
}

public function test_resolve_authorization_falls_back_to_redirect_http_authorization(): void
{
$auth = RequestContext::resolveAuthorizationHeader(
['REDIRECT_HTTP_AUTHORIZATION' => 'Bearer from-redirect'],
[],
);

$this->assertSame('Bearer from-redirect', $auth);
}

public function test_resolve_authorization_falls_back_to_request_headers(): void
{
// Apache mod_php withholds Authorization from $_SERVER entirely;
// the raw request headers (getallheaders()) are the only source.
$auth = RequestContext::resolveAuthorizationHeader(
['HTTP_HOST' => 'example.com'],
['Authorization' => 'Bearer from-headers'],
);

$this->assertSame('Bearer from-headers', $auth);
}

public function test_resolve_authorization_matches_header_name_case_insensitively(): void
{
$auth = RequestContext::resolveAuthorizationHeader(
[],
['authorization' => 'License from-lowercase'],
);

$this->assertSame('License from-lowercase', $auth);
}

public function test_resolve_authorization_returns_null_when_absent_everywhere(): void
{
$this->assertNull(RequestContext::resolveAuthorizationHeader(
['HTTP_HOST' => 'example.com'],
['User-Agent' => 'curl'],
));
}

public function test_from_globals_leaves_injection_only_signals_null(): void
{
$original = $_SERVER;
Expand Down
Loading