Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Fixed

- Reject invalid UTF-8 in documents https://github.com/webonyx/graphql-php/pull/1987

## v15.37.3

### Fixed
Expand Down
20 changes: 20 additions & 0 deletions src/Language/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ public function lookahead(): Token
*/
private function readToken(Token $prev): Token
{
if ($prev->kind === Token::SOF) {
$this->assertValidUTF8();
}

$bodyLength = $this->source->length;

$this->positionAfterWhitespace();
Expand Down Expand Up @@ -259,6 +263,22 @@ private function readToken(Token $prev): Token
throw new SyntaxError($this->source, $position, $this->unexpectedCharacterMessage($code));
}

/** @throws SyntaxError */
private function assertValidUTF8(): void
{
$body = $this->source->body;
if (mb_check_encoding($body, 'UTF-8')) {
return;
}

$scrubbedBody = mb_scrub($body, 'UTF-8');
$invalidByteOffset = strspn($body ^ $scrubbedBody, "\0");
$invalidByte = strtoupper(bin2hex($body[$invalidByteOffset]));
$validPrefix = substr($body, 0, $invalidByteOffset);

throw new SyntaxError($this->source, mb_strlen($validPrefix, 'UTF-8'), "Invalid UTF-8 byte: 0x{$invalidByte}");

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cause is older than this pull request: Source::getLocation() gives wrong columns for every syntax error after a multibyte line, e.g. # äää\nxx? reports 2:-2 on master. Fixed separately in #1988, after which I will add your case to this provider.

}

/** @throws \JsonException */
private function unexpectedCharacterMessage(?int $code): string
{
Expand Down
23 changes: 23 additions & 0 deletions tests/Language/LexerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,29 @@ public function testReportsUsefulUnknownCharErrors(string $str, string $expected
$this->expectSyntaxError($str, $expectedMessage, $location);
}

/** @return iterable<array{string, string, SourceLocation}> */
public static function reportsInvalidUTF8(): iterable
{
yield 'in name position' => ["\xFF", 'Invalid UTF-8 byte: 0xFF', self::loc(1, 1)];
yield 'in string' => ["\"a\xFFbcd\"", 'Invalid UTF-8 byte: 0xFF', self::loc(1, 3)];
yield 'after multibyte character' => ["\"ä\xC3x\"", 'Invalid UTF-8 byte: 0xC3', self::loc(1, 3)];
yield 'truncated at end' => ["\"ab\xE2\x82", 'Invalid UTF-8 byte: 0xE2', self::loc(1, 4)];
yield 'encoded surrogate' => ["\"\xED\xA0\x80\"", 'Invalid UTF-8 byte: 0xED', self::loc(1, 2)];
yield 'overlong encoding' => ["\"\xC0\xAF\"", 'Invalid UTF-8 byte: 0xC0', self::loc(1, 2)];
yield 'in block string' => ["\"\"\"\xFF\"\"\"", 'Invalid UTF-8 byte: 0xFF', self::loc(1, 4)];
yield 'in comment' => ["# \xFF\nfoo", 'Invalid UTF-8 byte: 0xFF', self::loc(1, 3)];
yield 'after the first token' => ["foo\n \"\xFF\"", 'Invalid UTF-8 byte: 0xFF', self::loc(2, 4)];
yield 'after BOM' => ["\u{FEFF}\xFF", 'Invalid UTF-8 byte: 0xFF', self::loc(1, 2)];
yield 'first of two' => ["\"a\xFEb\xFF\"", 'Invalid UTF-8 byte: 0xFE', self::loc(1, 3)];
yield 'after long multibyte prefix' => ['"' . str_repeat('€', 2000) . "\xFF\"", 'Invalid UTF-8 byte: 0xFF', self::loc(1, 2002)];
}

/** @dataProvider reportsInvalidUTF8 */
public function testReportsInvalidUTF8(string $str, string $expectedMessage, SourceLocation $location): void
{
$this->expectSyntaxError($str, $expectedMessage, $location);
}

/** @see it('lex reports useful information for dashes in names') */
public function testReportsUsefulDashesInfo(): void
{
Expand Down
Loading