diff --git a/src/MessageTrait.php b/src/MessageTrait.php index a7966d10..7bb8a05c 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -156,17 +156,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); } /** @@ -177,13 +182,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) { @@ -193,10 +198,20 @@ private function trimHeaderValues(array $values) )); } - return trim((string) $value, " \t"); + $trimmed = trim((string) $value, " \t"); + $this->assertValue($trimmed); + + return $trimmed; }, $values); } + /** + * @see https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @param mixed $header + * + * @return void + */ private function assertHeader($header) { if (!is_string($header)) { @@ -209,5 +224,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 c9b5957b..0d747c07 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -230,4 +230,54 @@ public function testAddsPortToHeaderAndReplacePreviousPort() $r = $r->withUri(new Uri('http://foo.com:8125/bar')); $this->assertEquals('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; + } }