From c8e7a38929abf690c3df44846c8ddc4ece5bc861 Mon Sep 17 00:00:00 2001 From: Rob Allen Date: Mon, 17 Apr 2023 17:02:20 +0100 Subject: [PATCH] Fix CVE-2023-30536 - GHSA-q2qj-628g-vhfw Security fix: Reject newlines at end of header names (cherry picked from commit ed1d553225dd190875d8814c47460daed4b550bb) --- src/Headers.php | 4 +-- tests/HeadersTest.php | 76 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/Headers.php b/src/Headers.php index afde205..b1c73d6 100644 --- a/src/Headers.php +++ b/src/Headers.php @@ -269,7 +269,7 @@ protected function validateHeader($name, $value): void */ protected function validateHeaderName($name): void { - if (!is_string($name) || preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $name) !== 1) { + if (!is_string($name) || preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $name) !== 1) { throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); } } @@ -289,7 +289,7 @@ protected function validateHeaderValue($value): void ); } - $pattern = "@^[ \t\x21-\x7E\x80-\xFF]*$@"; + $pattern = "@^[ \t\x21-\x7E\x80-\xFF]*$@D"; foreach ($items as $item) { $hasInvalidType = !is_numeric($item) && !is_string($item); $rejected = $hasInvalidType || preg_match($pattern, (string) $item) !== 1; diff --git a/tests/HeadersTest.php b/tests/HeadersTest.php index 972a0a0..f930c0f 100644 --- a/tests/HeadersTest.php +++ b/tests/HeadersTest.php @@ -209,4 +209,80 @@ public function testParseAuthorizationHeader() $headers = new Headers([], ['PHP_AUTH_DIGEST' => 'digest']); $this->assertEquals(['digest'], $headers->getHeader('Authorization')); } + + /** + * @dataProvider provideInvalidHeaderNames + */ + public function testWithInvalidHeaderName($headerName): void + { + $headers = new Headers(); + + $this->expectException(\InvalidArgumentException::class); + + $headers->setHeader($headerName, 'foo'); + } + + public static function provideInvalidHeaderNames(): array + { + return [ + [[]], + [false], + [new \stdClass()], + ["Content-Type\r\n\r\n"], + ["Content-Type\r\n"], + ["Content-Type\n"], + ["\r\nContent-Type"], + ["\nContent-Type"], + ["\n"], + ["\r\n"], + ["\t"], + ]; + } + + /** + * @dataProvider provideInvalidHeaderValues + */ + public function testSetInvalidHeaderValue($headerValue) + { + $headers = new Headers(); + + $this->expectException(\InvalidArgumentException::class); + + $headers->setHeader('Content-Type', $headerValue); + } + + public static function provideInvalidHeaderValues(): array + { + // Explicit tests for newlines as the most common exploit vector. + $tests = [ + ["new\nline"], + ["new\r\nline"], + ["new\rline"], + ["new\r\n line"], + ["newline\n"], + ["\nnewline"], + ["newline\r\n"], + ["\n\rnewline"], + ]; + + for ($i = 0; $i <= 0xff; $i++) { + if (\chr($i) == "\t") { + continue; + } + if (\chr($i) == " ") { + continue; + } + if ($i >= 0x21 && $i <= 0x7e) { + continue; + } + if ($i >= 0x80) { + continue; + } + + $tests[] = ["foo" . \chr($i) . "bar"]; + $tests[] = ["foo" . \chr($i)]; + } + + return $tests; + } }