Skip to content

Respect max_input_vars and max_input_nesting_level ini settings #268

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 2 commits into from
Nov 30, 2017
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,12 @@ See also [example #12](examples) for more details.

> PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware.

> This middleware respects the
[`max_input_vars`](http://php.net/manual/en/info.configuration.php#ini.max-input-vars)
(default `1000`) and
[`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level)
(default `64`) ini settings.

#### Third-Party Middleware

A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page.
Expand Down
41 changes: 41 additions & 0 deletions src/Io/MultipartParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ final class MultipartParser
*/
protected $maxFileSize;

/**
* ini setting "max_input_vars"
*
* Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here.
*
* @var int
* @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars
*/
private $maxInputVars = 1000;

/**
* ini setting "max_input_nesting_level"
*
* Does not exist in HHVM, but assumes hard coded to 64 (PHP's default).
*
* @var int
* @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level
*/
private $maxInputNestingLevel = 64;

private $postCount = 0;

public static function parseRequest(ServerRequestInterface $request)
{
$parser = new self($request);
Expand All @@ -36,6 +58,15 @@ public static function parseRequest(ServerRequestInterface $request)
private function __construct(ServerRequestInterface $request)
{
$this->request = $request;

$var = ini_get('max_input_vars');
if ($var !== false) {
$this->maxInputVars = (int)$var;
}
$var = ini_get('max_input_nesting_level');
if ($var !== false) {
$this->maxInputNestingLevel = (int)$var;
}
}

private function parse()
Expand Down Expand Up @@ -150,6 +181,11 @@ private function parseUploadedFile($filename, $contentType, $contents)

private function parsePost($name, $value)
{
// ignore excessive number of post fields
if (++$this->postCount > $this->maxInputVars) {
return;
}

$this->request = $this->request->withParsedBody($this->extractPost(
$this->request->getParsedBody(),
$name,
Expand Down Expand Up @@ -203,6 +239,11 @@ private function extractPost($postFields, $key, $value)
return $postFields;
}

// ignore this key if maximum nesting level is exceeded
if (isset($chunks[$this->maxInputNestingLevel])) {
return $postFields;
}

$chunkKey = rtrim($chunks[0], ']');
$parent = &$postFields;
for ($i = 1; isset($chunks[$i]); $i++) {
Expand Down
4 changes: 3 additions & 1 deletion src/Middleware/RequestBodyParserMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ public function __invoke(ServerRequestInterface $request, $next)

private function parseFormUrlencoded(ServerRequestInterface $request)
{
// parse string into array structure
// ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level)
$ret = array();
parse_str((string)$request->getBody(), $ret);
@parse_str((string)$request->getBody(), $ret);

return $request->withParsedBody($ret);
}
Expand Down
149 changes: 147 additions & 2 deletions tests/Middleware/RequestBodyParserMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,76 @@ function (ServerRequestInterface $request) {
$this->assertSame('foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar', (string)$parsedRequest->getBody());
}

public function testFormUrlencodedIgnoresBodyWithExcessiveNesting()
{
// supported in all Zend PHP versions and HHVM
// ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK
// HHVM limits to 64 and returns an empty array structure: https://3v4l.org/j3DK2
if (defined('HHVM_VERSION')) {
$this->markTestSkipped('Not supported on HHVM (limited to depth 64, but keeps empty array structure)');
}

$allowed = (int)ini_get('max_input_nesting_level');

$middleware = new RequestBodyParserMiddleware();
$request = new ServerRequest(
'POST',
'https://example.com/',
array(
'Content-Type' => 'application/x-www-form-urlencoded',
),
'hello' . str_repeat('[]', $allowed + 1) . '=world'
);

/** @var ServerRequestInterface $parsedRequest */
$parsedRequest = $middleware(
$request,
function (ServerRequestInterface $request) {
return $request;
}
);

$this->assertSame(
array(),
$parsedRequest->getParsedBody()
);
}

public function testFormUrlencodedTruncatesBodyWithExcessiveLength()
{
// supported as of PHP 5.3.11, no HHVM support: https://3v4l.org/PiqnQ
// ini setting already exists in PHP 5.3.9: https://3v4l.org/VF6oV
if (defined('HHVM_VERSION') || PHP_VERSION_ID < 50311) {
$this->markTestSkipped('Not supported on HHVM and PHP < 5.3.11 (unlimited length)');
}

$allowed = (int)ini_get('max_input_vars');

$middleware = new RequestBodyParserMiddleware();
$request = new ServerRequest(
'POST',
'https://example.com/',
array(
'Content-Type' => 'application/x-www-form-urlencoded',
),
str_repeat('a[]=b&', $allowed + 1)
);

/** @var ServerRequestInterface $parsedRequest */
$parsedRequest = $middleware(
$request,
function (ServerRequestInterface $request) {
return $request;
}
);

$body = $parsedRequest->getParsedBody();

$this->assertCount(1, $body);
$this->assertTrue(isset($body['a']));
$this->assertCount($allowed, $body['a']);
}

public function testDoesNotParseJsonByDefault()
{
$middleware = new RequestBodyParserMiddleware();
Expand Down Expand Up @@ -145,8 +215,7 @@ public function testMultipartFormDataParsing()
$data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n";
$data .= "\r\n";
$data .= "second\r\n";
$data .= "--$boundary\r\n";

$data .= "--$boundary--\r\n";

$request = new ServerRequest('POST', 'http://example.com/', array(
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
Expand All @@ -171,4 +240,80 @@ function (ServerRequestInterface $request) {
);
$this->assertSame($data, (string)$parsedRequest->getBody());
}

public function testMultipartFormDataIgnoresFieldWithExcessiveNesting()
{
// supported in all Zend PHP versions and HHVM
// ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK
// HHVM limits to 64 and otherwise returns an empty array structure
$allowed = (int)ini_get('max_input_nesting_level');
if ($allowed === 0) {
$allowed = 64;
}

$middleware = new RequestBodyParserMiddleware();

$boundary = "---------------------------12758086162038677464950549563";

$data = "--$boundary\r\n";
$data .= "Content-Disposition: form-data; name=\"hello" . str_repeat("[]", $allowed + 1) . "\"\r\n";
$data .= "\r\n";
$data .= "world\r\n";
$data .= "--$boundary--\r\n";

$request = new ServerRequest('POST', 'http://example.com/', array(
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
), $data, 1.1);

/** @var ServerRequestInterface $parsedRequest */
$parsedRequest = $middleware(
$request,
function (ServerRequestInterface $request) {
return $request;
}
);

$this->assertEmpty($parsedRequest->getParsedBody());
}

public function testMultipartFormDataTruncatesBodyWithExcessiveLength()
{
// ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV
// otherwise default to 1000 as implemented within
$allowed = (int)ini_get('max_input_vars');
if ($allowed === 0) {
$allowed = 1000;
}

$middleware = new RequestBodyParserMiddleware();

$boundary = "---------------------------12758086162038677464950549563";

$data = "";
for ($i = 0; $i < $allowed + 1; ++$i) {
$data .= "--$boundary\r\n";
$data .= "Content-Disposition: form-data; name=\"a[]\"\r\n";
$data .= "\r\n";
$data .= "b\r\n";
}
$data .= "--$boundary--\r\n";

$request = new ServerRequest('POST', 'http://example.com/', array(
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
), $data, 1.1);

/** @var ServerRequestInterface $parsedRequest */
$parsedRequest = $middleware(
$request,
function (ServerRequestInterface $request) {
return $request;
}
);

$body = $parsedRequest->getParsedBody();

$this->assertCount(1, $body);
$this->assertTrue(isset($body['a']));
$this->assertCount($allowed, $body['a']);
}
}