Skip to content

Commit 68f5388

Browse files
Add "headers requiring opt-in" for pass-through (#2007)
## 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]>
1 parent 00a4433 commit 68f5388

File tree

4 files changed

+155
-21
lines changed

4 files changed

+155
-21
lines changed

packages/playground/php-cors-proxy/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Request http://127.0.0.1:5263/proxy.php/https://w.org/?test=1 to get the respons
2525

2626
- Stream data both ways, don't buffer.
2727
- Don't pass auth headers in either direction.
28+
- Opt-in for request headers possible using `X-Cors-Proxy-Allowed-Request-Headers`.
2829
- Refuse to request private IPs.
2930
- Refuse to process non-GET non-POST non-OPTIONS requests.
3031
- Refuse to process POST request body larger than, say, 100KB.

packages/playground/php-cors-proxy/cors-proxy-functions.php

+59-12
Original file line numberDiff line numberDiff line change
@@ -299,20 +299,67 @@ private static function ipv6InRange($ip, $start, $end)
299299

300300
}
301301

302+
/**
303+
* Filters headers by name, removing disallowed headers and enforcing opt-in requirements.
304+
*
305+
* @param array $php_headers {
306+
* An associative array of headers.
307+
* @type string $key Header name.
308+
* }
309+
* @param array $disallowed_headers List of header names that are disallowed.
310+
* @param array $headers_requiring_opt_in List of header names that require opt-in
311+
* via the X-Cors-Proxy-Allowed-Request-Headers header.
312+
*
313+
* @return array {
314+
* Filtered headers.
315+
* @type string $key Header name.
316+
*/
317+
function filter_headers_by_name(
318+
$php_headers,
319+
$disallowed_headers,
320+
$headers_requiring_opt_in = [],
321+
) {
322+
$lowercased_php_headers = array_change_key_case($php_headers, CASE_LOWER);
323+
$disallowed_headers = array_map('strtolower', $disallowed_headers);
324+
$headers_requiring_opt_in = array_map('strtolower', $headers_requiring_opt_in);
325+
326+
// Get explicitly allowed headers from X-Cors-Proxy-Allowed-Request-Headers
327+
$headers_opt_in_str =
328+
$lowercased_php_headers['x-cors-proxy-allowed-request-headers'] ?? '';
329+
$headers_with_opt_in = $headers_opt_in_str
330+
? array_map('trim', explode(',', $headers_opt_in_str))
331+
: [];
332+
$headers_with_opt_in = array_map('strtolower', $headers_with_opt_in);
333+
334+
// Filter headers
335+
return array_filter(
336+
$php_headers,
337+
function (
338+
$key
339+
) use (
340+
$disallowed_headers,
341+
$headers_requiring_opt_in,
342+
$headers_with_opt_in,
343+
) {
344+
$lower_key = strtolower($key);
345+
346+
// Skip if disallowed
347+
if (in_array($lower_key, $disallowed_headers)) {
348+
return false;
349+
}
302350

303-
function filter_headers_strings($php_headers, $remove_headers) {
304-
$remove_headers = array_map('strtolower', $remove_headers);
305-
$headers = [];
306-
foreach ($php_headers as $header) {
307-
$lower_header = strtolower($header);
308-
foreach($remove_headers as $remove_header) {
309-
if (strpos($lower_header, $remove_header) === 0) {
310-
continue 2;
351+
// Skip if opt-in is required but not provided
352+
if (
353+
in_array($lower_key, $headers_requiring_opt_in) &&
354+
!in_array($lower_key, $headers_with_opt_in)
355+
) {
356+
return false;
311357
}
312-
}
313-
$headers[] = $header;
314-
}
315-
return $headers;
358+
359+
return true;
360+
},
361+
ARRAY_FILTER_USE_KEY
362+
);
316363
}
317364

318365
function kv_headers_to_curl_format($headers) {

packages/playground/php-cors-proxy/cors-proxy.php

+26-8
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,27 @@
101101
"$host:443:$resolvedIp"
102102
]);
103103

104-
// Pass all incoming headers except cookies and authorization
105-
$curlHeaders = filter_headers_strings(
106-
kv_headers_to_curl_format(getallheaders()),
107-
[
108-
'Cookie',
109-
'Authorization',
110-
'Host',
111-
]
104+
$strictly_disallowed_headers = [
105+
// Cookies represent a relationship between the proxy server
106+
// and the client, so it is inappropriate to forward them.
107+
'Cookie',
108+
// Drop the incoming Host header because it identifies the
109+
// proxy server, not the target server.
110+
'Host'
111+
];
112+
$headers_requiring_opt_in = [
113+
// Allow Authorization header to be forwarded only if the client
114+
// explicitly opts in to avoid undesirable situations such as:
115+
// - a browser auto-sending basic auth with every proxy request
116+
// - the proxy forwarding the basic auth values to all target servers
117+
'Authorization'
118+
];
119+
$curlHeaders = kv_headers_to_curl_format(
120+
filter_headers_by_name(
121+
getallheaders(),
122+
$strictly_disallowed_headers,
123+
$headers_requiring_opt_in,
124+
)
112125
);
113126
curl_setopt(
114127
$ch,
@@ -173,6 +186,11 @@ function(
173186
stripos($header, 'HTTP/') !== 0 &&
174187
stripos($header, 'Set-Cookie:') !== 0 &&
175188
stripos($header, 'Authorization:') !== 0 &&
189+
// The proxy server does not support relaying auth challenges.
190+
// Specifically, we want to avoid browsers prompting for basic auth
191+
// credentials which they will send to the proxy server for the
192+
// remainder of the session.
193+
stripos($header, 'WWW-Authenticate:') !== 0 &&
176194
stripos($header, 'Cache-Control:') !== 0 &&
177195
// The browser won't accept multiple values for these headers.
178196
stripos($header, 'Access-Control-Allow-Origin:') !== 0 &&

packages/playground/php-cors-proxy/tests/ProxyFunctionsTests.php

+69-1
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,72 @@ public function testUrlValidateAndResolveWithTargetSelf()
147147
'http://cors.playground.wordpress.net/cors-proxy.php?http://cors.playground.wordpress.net'
148148
);
149149
}
150-
}
150+
151+
public function testFilterHeadersStrings()
152+
{
153+
$original_headers = [
154+
'Accept' => 'application/json',
155+
'Content-Type' => 'application/json',
156+
'Cookie' => 'test=1',
157+
'Host' => 'example.com',
158+
];
159+
160+
$strictly_disallowed_headers = [
161+
'Cookie',
162+
'Host',
163+
];
164+
165+
$headers_requiring_opt_in = [
166+
'Authorization',
167+
];
168+
169+
$this->assertEquals(
170+
[
171+
'Accept' => 'application/json',
172+
'Content-Type' => 'application/json',
173+
],
174+
filter_headers_by_name(
175+
$original_headers,
176+
$strictly_disallowed_headers,
177+
$headers_requiring_opt_in,
178+
)
179+
);
180+
}
181+
182+
public function testFilterHeaderStringsWithAdditionalAllowedHeaders()
183+
{
184+
$original_headers = [
185+
'Accept' => 'application/json',
186+
'Content-Type' => 'application/json',
187+
'Cookie' => 'test=1',
188+
'Host' => 'example.com',
189+
'Authorization' => 'Bearer 1234567890',
190+
'X-Authorization' => 'Bearer 1234567890',
191+
'X-Cors-Proxy-Allowed-Request-Headers' => 'Authorization',
192+
];
193+
194+
$strictly_disallowed_headers = [
195+
'Cookie',
196+
'Host',
197+
];
198+
199+
$headers_requiring_opt_in = [
200+
'Authorization',
201+
];
202+
203+
$this->assertEquals(
204+
[
205+
'Accept' => 'application/json',
206+
'Content-Type' => 'application/json',
207+
'Authorization' => 'Bearer 1234567890',
208+
'X-Authorization' => 'Bearer 1234567890',
209+
'X-Cors-Proxy-Allowed-Request-Headers' => 'Authorization',
210+
],
211+
filter_headers_by_name(
212+
$original_headers,
213+
$strictly_disallowed_headers,
214+
$headers_requiring_opt_in,
215+
)
216+
);
217+
}
218+
}

0 commit comments

Comments
 (0)