Skip to content

Allow Authorization header pass-through with X-Cors-Proxy-Allowed-Request-Headers #2007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 12, 2024
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
1 change: 1 addition & 0 deletions packages/playground/php-cors-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Request http://127.0.0.1:5263/proxy.php/https://w.org/?test=1 to get the respons

- Stream data both ways, don't buffer.
- Don't pass auth headers in either direction.
- Opt-in for request headers possible using `X-Cors-Proxy-Allowed-Request-Headers`.
- Refuse to request private IPs.
- Refuse to process non-GET non-POST non-OPTIONS requests.
- Refuse to process POST request body larger than, say, 100KB.
Expand Down
71 changes: 59 additions & 12 deletions packages/playground/php-cors-proxy/cors-proxy-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,20 +299,67 @@ private static function ipv6InRange($ip, $start, $end)

}

/**
* Filters headers by name, removing disallowed headers and enforcing opt-in requirements.
*
* @param array $php_headers {
* An associative array of headers.
* @type string $key Header name.
* }
* @param array $disallowed_headers List of header names that are disallowed.
* @param array $headers_requiring_opt_in List of header names that require opt-in
* via the X-Cors-Proxy-Allowed-Request-Headers header.
*
* @return array {
* Filtered headers.
* @type string $key Header name.
*/
function filter_headers_by_name(
$php_headers,
$disallowed_headers,
$headers_requiring_opt_in = [],
) {
$lowercased_php_headers = array_change_key_case($php_headers, CASE_LOWER);
$disallowed_headers = array_map('strtolower', $disallowed_headers);
$headers_requiring_opt_in = array_map('strtolower', $headers_requiring_opt_in);

// Get explicitly allowed headers from X-Cors-Proxy-Allowed-Request-Headers
$headers_opt_in_str =
$lowercased_php_headers['x-cors-proxy-allowed-request-headers'] ?? '';
$headers_with_opt_in = $headers_opt_in_str
? array_map('trim', explode(',', $headers_opt_in_str))
: [];
$headers_with_opt_in = array_map('strtolower', $headers_with_opt_in);

// Filter headers
return array_filter(
$php_headers,
function (
$key
) use (
$disallowed_headers,
$headers_requiring_opt_in,
$headers_with_opt_in,
) {
$lower_key = strtolower($key);

// Skip if disallowed
if (in_array($lower_key, $disallowed_headers)) {
return false;
}

function filter_headers_strings($php_headers, $remove_headers) {
$remove_headers = array_map('strtolower', $remove_headers);
$headers = [];
foreach ($php_headers as $header) {
$lower_header = strtolower($header);
foreach($remove_headers as $remove_header) {
if (strpos($lower_header, $remove_header) === 0) {
continue 2;
// Skip if opt-in is required but not provided
if (
in_array($lower_key, $headers_requiring_opt_in) &&
!in_array($lower_key, $headers_with_opt_in)
) {
return false;
}
}
$headers[] = $header;
}
return $headers;

return true;
},
ARRAY_FILTER_USE_KEY
);
}

function kv_headers_to_curl_format($headers) {
Expand Down
34 changes: 26 additions & 8 deletions packages/playground/php-cors-proxy/cors-proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,27 @@
"$host:443:$resolvedIp"
]);

// Pass all incoming headers except cookies and authorization
$curlHeaders = filter_headers_strings(
kv_headers_to_curl_format(getallheaders()),
[
'Cookie',
'Authorization',
'Host',
]
$strictly_disallowed_headers = [
// Cookies represent a relationship between the proxy server
// and the client, so it is inappropriate to forward them.
'Cookie',
// Drop the incoming Host header because it identifies the
// proxy server, not the target server.
'Host'
];
$headers_requiring_opt_in = [
// Allow Authorization header to be forwarded only if the client
// explicitly opts in to avoid undesirable situations such as:
// - a browser auto-sending basic auth with every proxy request
// - the proxy forwarding the basic auth values to all target servers
'Authorization'
];
$curlHeaders = kv_headers_to_curl_format(
filter_headers_by_name(
getallheaders(),
$strictly_disallowed_headers,
$headers_requiring_opt_in,
)
);
curl_setopt(
$ch,
Expand Down Expand Up @@ -173,6 +186,11 @@ function(
stripos($header, 'HTTP/') !== 0 &&
stripos($header, 'Set-Cookie:') !== 0 &&
stripos($header, 'Authorization:') !== 0 &&
// The proxy server does not support relaying auth challenges.
// Specifically, we want to avoid browsers prompting for basic auth
// credentials which they will send to the proxy server for the
// remainder of the session.
stripos($header, 'WWW-Authenticate:') !== 0 &&
stripos($header, 'Cache-Control:') !== 0 &&
// The browser won't accept multiple values for these headers.
stripos($header, 'Access-Control-Allow-Origin:') !== 0 &&
Expand Down
70 changes: 69 additions & 1 deletion packages/playground/php-cors-proxy/tests/ProxyFunctionsTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,72 @@ public function testUrlValidateAndResolveWithTargetSelf()
'http://cors.playground.wordpress.net/cors-proxy.php?http://cors.playground.wordpress.net'
);
}
}

public function testFilterHeadersStrings()
{
$original_headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'test=1',
'Host' => 'example.com',
];

$strictly_disallowed_headers = [
'Cookie',
'Host',
];

$headers_requiring_opt_in = [
'Authorization',
];

$this->assertEquals(
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
filter_headers_by_name(
$original_headers,
$strictly_disallowed_headers,
$headers_requiring_opt_in,
)
);
}

public function testFilterHeaderStringsWithAdditionalAllowedHeaders()
{
$original_headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'test=1',
'Host' => 'example.com',
'Authorization' => 'Bearer 1234567890',
'X-Authorization' => 'Bearer 1234567890',
'X-Cors-Proxy-Allowed-Request-Headers' => 'Authorization',
];

$strictly_disallowed_headers = [
'Cookie',
'Host',
];

$headers_requiring_opt_in = [
'Authorization',
];

$this->assertEquals(
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer 1234567890',
'X-Authorization' => 'Bearer 1234567890',
'X-Cors-Proxy-Allowed-Request-Headers' => 'Authorization',
],
filter_headers_by_name(
$original_headers,
$strictly_disallowed_headers,
$headers_requiring_opt_in,
)
);
}
}
Loading