Skip to content

Commit 335ca4f

Browse files
authored
Merge commit from fork
[3.x] fix cases where a malformed HTTP request with 'Transfer-Encoding: chunked' could lead to ReactPHP looping forever
2 parents e544ae0 + 1e882ea commit 335ca4f

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

src/Io/ChunkedDecoder.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,20 @@ public function handleData($data)
156156
$this->transferredSize = 0;
157157
$this->buffer = (string)\substr($this->buffer, 2);
158158
} elseif ($this->chunkSize === 0) {
159+
if ($positionCrlf === false) {
160+
// end chunk received, but trailer is incomplete
161+
// trailer shouldn't be bigger than 1024 bytes
162+
if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) {
163+
$this->handleError(new Exception('Trailer size bigger than ' . static::MAX_CHUNK_HEADER_SIZE . ' bytes'));
164+
}
165+
return;
166+
}
159167
// end chunk received, skip all trailer data
160168
$this->buffer = (string)\substr($this->buffer, $positionCrlf);
161169
}
162170

163-
if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) {
164-
// the first 2 characters are not CRLF, send error event
171+
if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) >= 2) {
172+
// chunk is completely transferred, but the following two bytes are not a CRLF, send error event
165173
$this->handleError(new Exception('Chunk does not end with a CRLF'));
166174
return;
167175
}

tests/Io/ChunkedDecoderTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,57 @@ public function testEndChunkWithMultipleTrailersWillBeIgnored()
502502
$this->input->emit('data', ["0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n"]);
503503
}
504504

505+
public function testEndChunkWithIncompleteTrailerWithoutCrlfWillWaitForAdditionalDataAndNotCauseInfiniteLoop()
506+
{
507+
$this->parser->on('data', $this->expectCallableNever());
508+
$this->parser->on('error', $this->expectCallableNever());
509+
$this->parser->on('end', $this->expectCallableNever());
510+
$this->parser->on('close', $this->expectCallableNever());
511+
512+
// malformed end chunk with trailing data but without a terminating CRLF
513+
// must not loop forever, but wait for additional data
514+
$this->input->emit('data', ["0\r\nab"]);
515+
}
516+
517+
public function testEndChunkWithIncompleteTrailerWillEndOnceTrailerIsCompleted()
518+
{
519+
$this->parser->on('data', $this->expectCallableNever());
520+
$this->parser->on('error', $this->expectCallableNever());
521+
$this->parser->on('end', $this->expectCallableOnce());
522+
$this->parser->on('close', $this->expectCallableOnce());
523+
524+
$this->input->emit('data', ["0\r\nab"]);
525+
$this->input->emit('data', ["\r\n\r\n"]);
526+
}
527+
528+
public function testEndChunkWithIncompleteTrailerIsTooBig()
529+
{
530+
$this->parser->on('data', $this->expectCallableNever());
531+
$this->parser->on('close', $this->expectCallableOnce());
532+
$this->parser->on('end', $this->expectCallableNever());
533+
$this->parser->on('error', $this->expectCallableOnce());
534+
535+
$data = '';
536+
for ($i = 0; $i < 1025; $i++) {
537+
$data .= 'a';
538+
}
539+
540+
// incomplete trailer must not be buffered without any limit
541+
$this->input->emit('data', ["0\r\n" . $data]);
542+
}
543+
544+
public function testChunkFollowedByExactlyTwoNonCrlfBytesWillErrorAndNotCauseInfiniteLoop()
545+
{
546+
$this->parser->on('data', $this->expectCallableOnceWith('ab'));
547+
$this->parser->on('error', $this->expectCallableOnce());
548+
$this->parser->on('end', $this->expectCallableNever());
549+
$this->parser->on('close', $this->expectCallableOnce());
550+
551+
// completed chunk followed by exactly two bytes that are not a CRLF must not
552+
// loop forever, but report an invalid chunk terminator
553+
$this->input->emit('data', ["2\r\nabXY"]);
554+
}
555+
505556
public function testLeadingZerosInInvalidChunk()
506557
{
507558
$this->parser->on('data', $this->expectCallableNever());

0 commit comments

Comments
 (0)