Skip to content

Commit 69aea5a

Browse files
Fix arbitrary code execution in the compiled runtime (#119)
1 parent 77105da commit 69aea5a

7 files changed

Lines changed: 81 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG
22

3+
## 2.9.1 - Upcoming
4+
5+
* Fixed the compiled runtime to emit function names as string literals, preventing arbitrary code execution.
6+
* Fixed the parser to reject non-identifier function callees, such as literal and raw string callees.
7+
38
## 2.9.0 - 2026-06-10
49

510
* Added PHP 8.5 support.

‎src/CompilerRuntime.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515
class CompilerRuntime
1616
{
17-
const CACHE_VERSION = 4;
17+
const CACHE_VERSION = 5;
1818

1919
private $parser;
2020
private $compiler;

‎src/Parser.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ private function led_pipe(array $left)
274274

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

‎src/TreeCompiler.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@ private function visit_function(array $node)
257257
}
258258

259259
return $this->write(
260-
'$value = Fd::getInstance()->__invoke("%s", %s);',
261-
$node['value'], $args
260+
'$value = Fd::getInstance()->__invoke(%s, %s);',
261+
var_export($node['value'], true), $args
262262
);
263263
}
264264

‎tests/CompilerRuntimeTest.php‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use JmesPath\AstRuntime;
55
use JmesPath\CompilerRuntime;
6+
use JmesPath\SyntaxErrorException;
67
use PHPUnit\Framework\TestCase;
78

89
class CompilerRuntimeTest extends TestCase
@@ -29,7 +30,7 @@ public function testFunctionNameUsesVersionedSalt(): void
2930
{
3031
$this->assertSame(
3132
'jmespath_' . md5(
32-
'jmespath:' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . ':4:foo.bar'
33+
'jmespath:' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . ':5:foo.bar'
3334
),
3435
CompilerRuntime::functionName('foo.bar')
3536
);
@@ -162,6 +163,35 @@ public function testCompiledRuntimeSortsNumerically(): void
162163
}
163164
}
164165

166+
public function testRejectsLiteralFunctionName(): void
167+
{
168+
$dir = $this->createTempDir();
169+
170+
try {
171+
$runtime = new CompilerRuntime($dir);
172+
$runtime('`"not_a_function"`(@)', []);
173+
$this->fail('Expected SyntaxErrorException');
174+
} catch (SyntaxErrorException $e) {
175+
$this->assertSame([], glob($dir . '/*') ?: []);
176+
} finally {
177+
$this->removeTempDir($dir);
178+
}
179+
}
180+
181+
public function testStillCompilesValidFunctions(): void
182+
{
183+
$dir = $this->createTempDir();
184+
185+
try {
186+
$runtime = new CompilerRuntime($dir);
187+
188+
$this->assertSame(3, $runtime('length(@)', [1, 2, 3]));
189+
$this->assertTrue($runtime("contains(@, 'b')", ['a', 'b', 'c']));
190+
} finally {
191+
$this->removeTempDir($dir);
192+
}
193+
}
194+
165195
private function createTempDir()
166196
{
167197
$dir = sys_get_temp_dir() . '/jmespath-compiler-' . bin2hex(random_bytes(12));

‎tests/ParserTest.php‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public static function invalidExpressionProvider(): array
3636
['>', 'Syntax error at character 0'],
3737
['|', 'Syntax error at character 0'],
3838
['@(foo)', 'Invalid function name'],
39+
['`"not_a_function"`(@)', 'Invalid function name'],
40+
["'not_a_function'(@)", 'Invalid function name'],
3941
['@=', 'Did not reach the end of the token stream'],
4042
['`1` `2`', 'Did not reach the end of the token stream'],
4143
['{a: @', 'Syntax error at character 5'],
@@ -69,6 +71,22 @@ public function testInvalidExpressionsThrowCleanSyntaxErrors(string $expr): void
6971
$this->assertSame([], $diags, "PHP warnings/notices emitted while parsing: $expr");
7072
}
7173

74+
public function testParsesIdentifierFunctionName(): void
75+
{
76+
$parser = new Parser();
77+
78+
$this->assertSame(
79+
[
80+
'type' => 'function',
81+
'value' => 'length',
82+
'children' => [
83+
['type' => 'current'],
84+
],
85+
],
86+
$parser->parse('length(@)')
87+
);
88+
}
89+
7290
public static function invalidLedTokenProvider(): array
7391
{
7492
return [

‎tests/TreeCompilerTest.php‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,27 @@ public function testFromlessProjectionUsesCorrectGuard(): void
4242
$source
4343
);
4444
}
45+
46+
public function testEscapesFunctionName(): void
47+
{
48+
$compiler = new TreeCompiler();
49+
$source = $compiler->visit(
50+
[
51+
'type' => 'function',
52+
'value' => '" . $shouldNotBeCode . "',
53+
'children' => [],
54+
],
55+
'testing',
56+
'example'
57+
);
58+
59+
$this->assertStringContainsString(
60+
'$value = Fd::getInstance()->__invoke(\'" . $shouldNotBeCode . "\', $args);',
61+
$source
62+
);
63+
$this->assertStringNotContainsString(
64+
'__invoke("" . $shouldNotBeCode . "", $args);',
65+
$source
66+
);
67+
}
4568
}

0 commit comments

Comments
 (0)