Skip to content

Commit

Permalink
Add "headers requiring opt-in" for pass-through (#2007)
Browse files Browse the repository at this point in the history
## Motivation for the change, related issues

We're building a plugin that pulls remote data into WordPress during
page render using Block Bindings and most endpoints require an
Authorization header.

## Implementation details

The code speaks for itself, but it's just a single line change to remove
`Authorization` from the array of headers that get stripped from the
request.

## Testing Instructions (or ideally a Blueprint)

Make a remote request from playground that sends an Authorization header
and see that the header makes it to the remote host.

---------

Co-authored-by: Brandon Payton <[email protected]>
  • Loading branch information
maxschmeling and brandonpayton authored Dec 12, 2024
1 parent 00a4433 commit 68f5388
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 21 deletions.
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,
)
);
}
}

0 comments on commit 68f5388

Please sign in to comment.