Skip to content

Commit

Permalink
fix: allow strict checks for Content-* headers
Browse files Browse the repository at this point in the history
Previously, we would check any key starting with `CONTENT_` in the `$_SERVER` array, and treat it as a header.
However, per laminas#63, this is incorrect, and we should only look for `CONTENT_TYPE`, `CONTENT_LENGTH`, and `CONTENT_MD5`.
To keep backwards compatibility, this patch implements a switch, using the ENV variable LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP.
When this value is found in `$_SERVER` (which aggregates ENV variables as well), the logic for identifying headers in the `$_SERVER` array will become strict.

For version 3.0, we will make the strict lookup the default.

Signed-off-by: Matthew Weier O'Phinney <matthew@weierophinney.net>
  • Loading branch information
weierophinney committed May 17, 2021
1 parent bcb2daf commit 1e89a68
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 1 deletion.
21 changes: 21 additions & 0 deletions docs/book/v2/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ in the name were renamed with underlines. By getting the cookies directly from t
access to the original cookies in the way you set them in your application and they are send by the user
agent.

> #### Strict Content- header matching
>
> Available since version 2.6.0
>
> By default, Diactoros will resolve any `$_SERVER` keys matching the prefix `CONTENT_` as HTTP headers.
> However, the proper behavior is to only match `CONTENT_TYPE`, `CONTENT_LENGTH`, and `CONTENT_MD5`, mapping them to `Content-Type`, `Content-Length`, and `Content-MD5` headers, respectively.
> Since changing the existing behavior may break some applications, we will not make the functionality more restrictive before version 3.0.0.
> If you are running into issues whereby you have ENV variables that are being munged into request headers, you can define the following ENV variable in your application to enable the more strict behavior:
>
> - LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP
>
> As an example, you could define it in your application's `.env` file if you are using [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv):
>
> ```env
> LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP=true
> ```
>
> Alternately, you could define it as a php-fpm or Apache environment variable.
>
> Once this ENV variable is present, the logic for identifying `Content-*` headers will only look at the `CONTENT_TYPE`, `CONTENT_LENGTH`, and `CONTENT_MD5` variables in `$_SERVER`, and skip over any others.
### Manipulating the Response

Use the response object to add headers and provide content for the response. Writing to the body
Expand Down
15 changes: 14 additions & 1 deletion src/functions/marshal_headers_from_sapi.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
*/
function marshalHeadersFromSapi(array $server) : array
{
$contentHeaderLookup = isset($server['LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP'])
? function (string $key) : bool {
static $contentHeaders = [
'CONTENT_TYPE' => true,
'CONTENT_LENGTH' => true,
'CONTENT_MD5' => true,
];
return isset($contentHeaders[$key]);
}
: function (string $key): bool {
return strpos($key, 'CONTENT_') === 0;
};

$headers = [];
foreach ($server as $key => $value) {
if (! is_string($key)) {
Expand Down Expand Up @@ -45,7 +58,7 @@ function marshalHeadersFromSapi(array $server) : array
continue;
}

if (strpos($key, 'CONTENT_') === 0) {
if ($contentHeaderLookup($key)) {
$name = strtr(strtolower($key), '_', '-');
$headers[$name] = $value;
continue;
Expand Down
100 changes: 100 additions & 0 deletions test/ServerRequestFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,4 +622,104 @@ public function testServerRequestFactoryHasAWritableEmptyBody()
$this->assertTrue($body->isSeekable());
$this->assertSame(0, $body->getSize());
}

/**
* @psalm-return iterable<string, array{
* 0: array<string, string>,
* 1: string,
* 2: string,
* 3: string
* }>
*/
public function serverContentMap(): iterable
{
yield 'content-type' => [
[
'HTTP_CONTENT_TYPE' => 'text/plain',
'CONTENT_TYPE' => 'application/x-octect-stream',
],
'CONTENT_TYPE',
'application/x-octect-stream',
'application/x-octect-stream',
];

yield 'content-length' => [
[
'HTTP_CONTENT_LENGTH' => '24',
'CONTENT_LENGTH' => '42',
],
'CONTENT_LENGTH',
'42',
'42',
];

yield 'content-md5' => [
[
'HTTP_CONTENT_MD5' => '3112373cbdba2b74d26d231f1aa5318b',
'CONTENT_MD5' => 'a918b672e563fb911e8c59ea1c56819a',
],
'CONTENT_MD5',
'a918b672e563fb911e8c59ea1c56819a',
'a918b672e563fb911e8c59ea1c56819a',
];

yield 'env-value-last-default-behavior' => [
[
'HTTP_CONTENT_API_PASSWORD' => 'password from header',
'CONTENT_API_PASSWORD' => 'password from env',
],
'CONTENT_API_PASSWORD',
'password from env',
'password from env',
];

yield 'env-value-first-default-behavior' => [
[
'CONTENT_API_PASSWORD' => 'password from env',
'HTTP_CONTENT_API_PASSWORD' => 'password from header',
],
'CONTENT_API_PASSWORD',
'password from header',
'password from env',
];

yield 'env-value-last-strict-content-headers' => [
[
'HTTP_CONTENT_API_PASSWORD' => 'password from header',
'CONTENT_API_PASSWORD' => 'password from env',
'LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP' => 'true',
],
'CONTENT_API_PASSWORD',
'password from header',
'password from env',
];

yield 'env-value-first-strict-content-headers' => [
[
'CONTENT_API_PASSWORD' => 'password from env',
'LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP' => 'true',
'HTTP_CONTENT_API_PASSWORD' => 'password from header',
],
'CONTENT_API_PASSWORD',
'password from header',
'password from env',
];
}

/**
* @dataProvider serverContentMap
* @psalm-param array<string, string> $server
*/
public function testDoesNotMarshalAllContentPrefixedServerVarsAsHeaders(
array $server,
string $key,
string $expectedHeaderValue,
string $expectedServerValue
): void {
$request = ServerRequestFactory::fromGlobals($server);
$headerName = str_replace('_', '-', $key);

$this->assertSame($expectedHeaderValue, $request->getHeaderLine($headerName));
$this->assertSame($expectedServerValue, $request->getServerParams()[$key]);
}
}

0 comments on commit 1e89a68

Please sign in to comment.