diff --git a/src/Node/Expression/MacroReferenceExpression.php b/src/Node/Expression/MacroReferenceExpression.php index ba1d556b4..59cb2092a 100644 --- a/src/Node/Expression/MacroReferenceExpression.php +++ b/src/Node/Expression/MacroReferenceExpression.php @@ -26,6 +26,14 @@ class MacroReferenceExpression extends AbstractExpression implements SupportDefi public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno) { + // The name is emitted as raw PHP in compile() via "->{$name}(...)", + // so it must be a valid PHP method identifier. Reject anything else + // as a defense-in-depth against accidental PHP code injection from + // a caller that forgot to validate user-controlled input. + if (!preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $name)) { + throw new \LogicException(\sprintf('Macro name "%s" is not a valid PHP identifier.', $name)); + } + parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name], $lineno); } diff --git a/tests/Node/Expression/MacroReferenceTest.php b/tests/Node/Expression/MacroReferenceTest.php new file mode 100644 index 000000000..817542c5b --- /dev/null +++ b/tests/Node/Expression/MacroReferenceTest.php @@ -0,0 +1,41 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage(\sprintf('Macro name "%s" is not a valid PHP identifier.', $name)); + + new MacroReferenceExpression(new TemplateVariable('foo', 1), $name, new ArrayExpression([], 1), 1); + } + + public static function provideInvalidMacroNames(): iterable + { + yield 'empty' => ['']; + yield 'starts with digit' => ['1foo']; + yield 'contains space' => ['foo bar']; + yield 'contains semicolon' => ['foo;bar']; + yield 'PHP injection payload' => ['macro_foo + 1; trigger_error("BAD") //']; + yield 'contains NUL byte' => ["foo\x00bar"]; + } +}