Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## 2.9.1 - Upcoming

* Fixed the compiled runtime to emit function names as string literals, preventing arbitrary code execution.
* Fixed the parser to reject non-identifier function callees, such as literal and raw string callees.

## 2.9.0 - 2026-06-10

* Added PHP 8.5 support.
Expand Down
2 changes: 1 addition & 1 deletion src/CompilerRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class CompilerRuntime
{
const CACHE_VERSION = 4;
const CACHE_VERSION = 5;

private $parser;
private $compiler;
Expand Down
2 changes: 1 addition & 1 deletion src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ private function led_pipe(array $left)

private function led_lparen(array $left)
{
if (!isset($left['value'])) {
if (!isset($left['type'], $left['value']) || $left['type'] !== 'field') {
throw $this->syntax('Invalid function name');
}

Expand Down
4 changes: 2 additions & 2 deletions src/TreeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ private function visit_function(array $node)
}

return $this->write(
'$value = Fd::getInstance()->__invoke("%s", %s);',
$node['value'], $args
'$value = Fd::getInstance()->__invoke(%s, %s);',
var_export($node['value'], true), $args
);
}

Expand Down
32 changes: 31 additions & 1 deletion tests/CompilerRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use JmesPath\AstRuntime;
use JmesPath\CompilerRuntime;
use JmesPath\SyntaxErrorException;
use PHPUnit\Framework\TestCase;

class CompilerRuntimeTest extends TestCase
Expand All @@ -29,7 +30,7 @@ public function testFunctionNameUsesVersionedSalt(): void
{
$this->assertSame(
'jmespath_' . md5(
'jmespath:' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . ':4:foo.bar'
'jmespath:' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . ':5:foo.bar'
),
CompilerRuntime::functionName('foo.bar')
);
Expand Down Expand Up @@ -162,6 +163,35 @@ public function testCompiledRuntimeSortsNumerically(): void
}
}

public function testRejectsLiteralFunctionName(): void
{
$dir = $this->createTempDir();

try {
$runtime = new CompilerRuntime($dir);
$runtime('`"not_a_function"`(@)', []);
$this->fail('Expected SyntaxErrorException');
} catch (SyntaxErrorException $e) {
$this->assertSame([], glob($dir . '/*') ?: []);
} finally {
$this->removeTempDir($dir);
}
}

public function testStillCompilesValidFunctions(): void
{
$dir = $this->createTempDir();

try {
$runtime = new CompilerRuntime($dir);

$this->assertSame(3, $runtime('length(@)', [1, 2, 3]));
$this->assertTrue($runtime("contains(@, 'b')", ['a', 'b', 'c']));
} finally {
$this->removeTempDir($dir);
}
}

private function createTempDir()
{
$dir = sys_get_temp_dir() . '/jmespath-compiler-' . bin2hex(random_bytes(12));
Expand Down
18 changes: 18 additions & 0 deletions tests/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public static function invalidExpressionProvider(): array
['>', 'Syntax error at character 0'],
['|', 'Syntax error at character 0'],
['@(foo)', 'Invalid function name'],
['`"not_a_function"`(@)', 'Invalid function name'],
["'not_a_function'(@)", 'Invalid function name'],
['@=', 'Did not reach the end of the token stream'],
['`1` `2`', 'Did not reach the end of the token stream'],
['{a: @', 'Syntax error at character 5'],
Expand Down Expand Up @@ -69,6 +71,22 @@ public function testInvalidExpressionsThrowCleanSyntaxErrors(string $expr): void
$this->assertSame([], $diags, "PHP warnings/notices emitted while parsing: $expr");
}

public function testParsesIdentifierFunctionName(): void
{
$parser = new Parser();

$this->assertSame(
[
'type' => 'function',
'value' => 'length',
'children' => [
['type' => 'current'],
],
],
$parser->parse('length(@)')
);
}

public static function invalidLedTokenProvider(): array
{
return [
Expand Down
23 changes: 23 additions & 0 deletions tests/TreeCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,27 @@ public function testFromlessProjectionUsesCorrectGuard(): void
$source
);
}

public function testEscapesFunctionName(): void
{
$compiler = new TreeCompiler();
$source = $compiler->visit(
[
'type' => 'function',
'value' => '" . $shouldNotBeCode . "',
'children' => [],
],
'testing',
'example'
);

$this->assertStringContainsString(
'$value = Fd::getInstance()->__invoke(\'" . $shouldNotBeCode . "\', $args);',
$source
);
$this->assertStringNotContainsString(
'__invoke("" . $shouldNotBeCode . "", $args);',
$source
);
}
}
Loading