Skip to content

Commit 902db15

Browse files
Release 1.8.4 (#486)
Co-authored-by: Tim Düsterhus <[email protected]>
1 parent 1afdd86 commit 902db15

File tree

4 files changed

+121
-6
lines changed

4 files changed

+121
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
## Unreleased
1111

12+
## 1.8.4 - 2022-03-20
13+
14+
### Fixed
15+
16+
- Validate header values properly
17+
1218
## 1.8.3 - 2021-10-05
1319

1420
### Fixed

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
},
6969
"config": {
7070
"preferred-install": "dist",
71-
"sort-packages": true
71+
"sort-packages": true,
72+
"allow-plugins": {
73+
"bamarni/composer-bin-plugin": true
74+
}
7275
}
7376
}

src/MessageTrait.php

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,22 @@ private function setHeaders(array $headers)
157157
}
158158
}
159159

160+
/**
161+
* @param mixed $value
162+
*
163+
* @return string[]
164+
*/
160165
private function normalizeHeaderValue($value)
161166
{
162167
if (!is_array($value)) {
163-
return $this->trimHeaderValues([$value]);
168+
return $this->trimAndValidateHeaderValues([$value]);
164169
}
165170

166171
if (count($value) === 0) {
167172
throw new \InvalidArgumentException('Header value can not be an empty array.');
168173
}
169174

170-
return $this->trimHeaderValues($value);
175+
return $this->trimAndValidateHeaderValues($value);
171176
}
172177

173178
/**
@@ -178,13 +183,13 @@ private function normalizeHeaderValue($value)
178183
* header-field = field-name ":" OWS field-value OWS
179184
* OWS = *( SP / HTAB )
180185
*
181-
* @param string[] $values Header values
186+
* @param mixed[] $values Header values
182187
*
183188
* @return string[] Trimmed header values
184189
*
185190
* @see https://tools.ietf.org/html/rfc7230#section-3.2.4
186191
*/
187-
private function trimHeaderValues(array $values)
192+
private function trimAndValidateHeaderValues(array $values)
188193
{
189194
return array_map(function ($value) {
190195
if (!is_scalar($value) && null !== $value) {
@@ -194,10 +199,20 @@ private function trimHeaderValues(array $values)
194199
));
195200
}
196201

197-
return trim((string) $value, " \t");
202+
$trimmed = trim((string) $value, " \t");
203+
$this->assertValue($trimmed);
204+
205+
return $trimmed;
198206
}, array_values($values));
199207
}
200208

209+
/**
210+
* @see https://tools.ietf.org/html/rfc7230#section-3.2
211+
*
212+
* @param mixed $header
213+
*
214+
* @return void
215+
*/
201216
private function assertHeader($header)
202217
{
203218
if (!is_string($header)) {
@@ -210,5 +225,46 @@ private function assertHeader($header)
210225
if ($header === '') {
211226
throw new \InvalidArgumentException('Header name can not be empty.');
212227
}
228+
229+
if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $header)) {
230+
throw new \InvalidArgumentException(
231+
sprintf(
232+
'"%s" is not valid header name',
233+
$header
234+
)
235+
);
236+
}
237+
}
238+
239+
/**
240+
* @param string $value
241+
*
242+
* @return void
243+
*
244+
* @see https://tools.ietf.org/html/rfc7230#section-3.2
245+
*
246+
* field-value = *( field-content / obs-fold )
247+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
248+
* field-vchar = VCHAR / obs-text
249+
* VCHAR = %x21-7E
250+
* obs-text = %x80-FF
251+
* obs-fold = CRLF 1*( SP / HTAB )
252+
*/
253+
private function assertValue($value)
254+
{
255+
// The regular expression intentionally does not support the obs-fold production, because as
256+
// per RFC 7230#3.2.4:
257+
//
258+
// A sender MUST NOT generate a message that includes
259+
// line folding (i.e., that has any field-value that contains a match to
260+
// the obs-fold rule) unless the message is intended for packaging
261+
// within the message/http media type.
262+
//
263+
// Clients must not send a request with line folding and a server sending folded headers is
264+
// likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting
265+
// folding is not likely to break any legitimate use case.
266+
if (! preg_match('/^(?:[\x21-\x7E\x80-\xFF](?:[\x20\x09]+[\x21-\x7E\x80-\xFF])?)*$/', $value)) {
267+
throw new \InvalidArgumentException(sprintf('"%s" is not valid header value', $value));
268+
}
213269
}
214270
}

tests/RequestTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,54 @@ public function testAddsPortToHeaderAndReplacePreviousPort()
229229
$r = $r->withUri(new Uri('http://foo.com:8125/bar'));
230230
self::assertSame('foo.com:8125', $r->getHeaderLine('host'));
231231
}
232+
233+
/**
234+
* @dataProvider provideHeaderValuesContainingNotAllowedChars
235+
*/
236+
public function testContainsNotAllowedCharsOnHeaderValue($value)
237+
{
238+
$this->expectExceptionGuzzle('InvalidArgumentException', sprintf('"%s" is not valid header value', $value));
239+
$r = new Request(
240+
'GET',
241+
'http://foo.com/baz?bar=bam',
242+
[
243+
'testing' => $value
244+
]
245+
);
246+
}
247+
248+
/**
249+
* @return iterable
250+
*/
251+
public function provideHeaderValuesContainingNotAllowedChars()
252+
{
253+
// Explicit tests for newlines as the most common exploit vector.
254+
$tests = [
255+
["new\nline"],
256+
["new\r\nline"],
257+
["new\rline"],
258+
// Line folding is technically allowed, but deprecated.
259+
// We don't support it.
260+
["new\r\n line"],
261+
];
262+
263+
for ($i = 0; $i <= 0xff; $i++) {
264+
if (\chr($i) == "\t") {
265+
continue;
266+
}
267+
if (\chr($i) == " ") {
268+
continue;
269+
}
270+
if ($i >= 0x21 && $i <= 0x7e) {
271+
continue;
272+
}
273+
if ($i >= 0x80) {
274+
continue;
275+
}
276+
277+
$tests[] = ["foo" . \chr($i) . "bar"];
278+
}
279+
280+
return $tests;
281+
}
232282
}

0 commit comments

Comments
 (0)