From 902db15a551a4a415e732b622282e21ce1b508b4 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 20 Mar 2022 13:44:44 +0000 Subject: [PATCH] Release 1.8.4 (#486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Düsterhus --- CHANGELOG.md | 6 ++++ composer.json | 5 +++- src/MessageTrait.php | 66 +++++++++++++++++++++++++++++++++++++++---- tests/RequestTest.php | 50 ++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40736c4..e5f80274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +## 1.8.4 - 2022-03-20 + +### Fixed + +- Validate header values properly + ## 1.8.3 - 2021-10-05 ### Fixed diff --git a/composer.json b/composer.json index bfa7cfdc..7ecdc8ba 100644 --- a/composer.json +++ b/composer.json @@ -68,6 +68,9 @@ }, "config": { "preferred-install": "dist", - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } } } diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 99203bb4..459b104d 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -157,17 +157,22 @@ private function setHeaders(array $headers) } } + /** + * @param mixed $value + * + * @return string[] + */ private function normalizeHeaderValue($value) { if (!is_array($value)) { - return $this->trimHeaderValues([$value]); + return $this->trimAndValidateHeaderValues([$value]); } if (count($value) === 0) { throw new \InvalidArgumentException('Header value can not be an empty array.'); } - return $this->trimHeaderValues($value); + return $this->trimAndValidateHeaderValues($value); } /** @@ -178,13 +183,13 @@ private function normalizeHeaderValue($value) * header-field = field-name ":" OWS field-value OWS * OWS = *( SP / HTAB ) * - * @param string[] $values Header values + * @param mixed[] $values Header values * * @return string[] Trimmed header values * * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 */ - private function trimHeaderValues(array $values) + private function trimAndValidateHeaderValues(array $values) { return array_map(function ($value) { if (!is_scalar($value) && null !== $value) { @@ -194,10 +199,20 @@ private function trimHeaderValues(array $values) )); } - return trim((string) $value, " \t"); + $trimmed = trim((string) $value, " \t"); + $this->assertValue($trimmed); + + return $trimmed; }, array_values($values)); } + /** + * @see https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @param mixed $header + * + * @return void + */ private function assertHeader($header) { if (!is_string($header)) { @@ -210,5 +225,46 @@ private function assertHeader($header) if ($header === '') { throw new \InvalidArgumentException('Header name can not be empty.'); } + + if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $header)) { + throw new \InvalidArgumentException( + sprintf( + '"%s" is not valid header name', + $header + ) + ); + } + } + + /** + * @param string $value + * + * @return void + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2 + * + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * VCHAR = %x21-7E + * obs-text = %x80-FF + * obs-fold = CRLF 1*( SP / HTAB ) + */ + private function assertValue($value) + { + // The regular expression intentionally does not support the obs-fold production, because as + // per RFC 7230#3.2.4: + // + // A sender MUST NOT generate a message that includes + // line folding (i.e., that has any field-value that contains a match to + // the obs-fold rule) unless the message is intended for packaging + // within the message/http media type. + // + // Clients must not send a request with line folding and a server sending folded headers is + // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting + // folding is not likely to break any legitimate use case. + if (! preg_match('/^(?:[\x21-\x7E\x80-\xFF](?:[\x20\x09]+[\x21-\x7E\x80-\xFF])?)*$/', $value)) { + throw new \InvalidArgumentException(sprintf('"%s" is not valid header value', $value)); + } } } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 1e06abcd..d943dc50 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -229,4 +229,54 @@ public function testAddsPortToHeaderAndReplacePreviousPort() $r = $r->withUri(new Uri('http://foo.com:8125/bar')); self::assertSame('foo.com:8125', $r->getHeaderLine('host')); } + + /** + * @dataProvider provideHeaderValuesContainingNotAllowedChars + */ + public function testContainsNotAllowedCharsOnHeaderValue($value) + { + $this->expectExceptionGuzzle('InvalidArgumentException', sprintf('"%s" is not valid header value', $value)); + $r = new Request( + 'GET', + 'http://foo.com/baz?bar=bam', + [ + 'testing' => $value + ] + ); + } + + /** + * @return iterable + */ + public function provideHeaderValuesContainingNotAllowedChars() + { + // Explicit tests for newlines as the most common exploit vector. + $tests = [ + ["new\nline"], + ["new\r\nline"], + ["new\rline"], + // Line folding is technically allowed, but deprecated. + // We don't support it. + ["new\r\n line"], + ]; + + 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"]; + } + + return $tests; + } }