API Platform version(s) affected: 4.3.5
Description
ResourceAccessChecker accepts a nullable expression language (?ExpressionLanguage $expressionLanguage = null) and handles the null case in isGranted() — it throws a descriptive LogicException. usesObjectVariable() does not: it calls $this->expressionLanguage->parse(...) directly, so when the expression language is null it fatals with "Call to a member function parse() on null" instead of the same graceful error. AccessCheckerProvider invokes usesObjectVariable() while evaluating any operation carrying a security: expression, so the path is reachable during normal access checking.
// src/Symfony/Security/ResourceAccessChecker.php
public function __construct(private readonly ?ExpressionLanguage $expressionLanguage = null, /* … */) {}
public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool
{
// …
if (null === $this->expressionLanguage) { // guarded
throw new \LogicException('The "symfony/expression-language" library must be installed to use the "security" attribute.');
}
return (bool) $this->expressionLanguage->evaluate($expression, $this->getVariables($extraVariables));
}
public function usesObjectVariable(string $expression, array $variables = []): bool
{
// not guarded → ->parse() on a possibly-null property
return $this->hasObjectVariable($this->expressionLanguage->parse($expression, array_keys($this->getVariables($variables)))->getNodes()->toArray());
}
How to reproduce
$expressionLanguage is null whenever security.expression_language is unavailable at runtime. Two ways to hit it:
-
symfony/expression-language not installed — the case the isGranted() guard message already targets. Define a resource with security: "is_granted(...)" and request it → usesObjectVariable() fatals before the isGranted() guard can report the missing library.
-
Service pruned even when installed. The dependency is a soft reference:
// src/Symfony/Bundle/Resources/config/security.php
$services->alias('api_platform.security.expression_language', 'security.expression_language');
$services->set('api_platform.security.resource_access_checker', ResourceAccessChecker::class)
->args([
service('api_platform.security.expression_language')->nullOnInvalid(),
// …
]);
In a kernel that doesn't register access_control / the Symfony expression voter, ResourceAccessChecker is the only consumer of security.expression_language. Because the reference is nullOnInvalid(), RemoveUnusedDefinitionsPass removes the private definition and the argument resolves to null. Compile the container (e.g. prod) and request any security:-protected operation → fatal in usesObjectVariable().
Possible Solution
Mirror the existing isGranted() guard in usesObjectVariable():
public function usesObjectVariable(string $expression, array $variables = []): bool
{
if (null === $this->expressionLanguage) {
throw new \LogicException('The "symfony/expression-language" library must be installed to use the "security" attribute.');
}
return $this->hasObjectVariable($this->expressionLanguage->parse($expression, array_keys($this->getVariables($variables)))->getNodes()->toArray());
}
Happy to open a PR.
Additional Context
Whether the service should be pruned in case (2) is arguably the consuming kernel's responsibility (it owns its container). This report is narrower: the asymmetric null handling inside ResourceAccessChecker is self-contained — the constructor permits null and isGranted() already anticipates it, so usesObjectVariable() should handle it the same way.
Environment: Symfony security-core 8.0.12, PHP 8.4.
API Platform version(s) affected: 4.3.5
Description
ResourceAccessCheckeraccepts a nullable expression language (?ExpressionLanguage $expressionLanguage = null) and handles the null case inisGranted()— it throws a descriptiveLogicException.usesObjectVariable()does not: it calls$this->expressionLanguage->parse(...)directly, so when the expression language is null it fatals with "Call to a member function parse() on null" instead of the same graceful error.AccessCheckerProviderinvokesusesObjectVariable()while evaluating any operation carrying asecurity:expression, so the path is reachable during normal access checking.How to reproduce
$expressionLanguageis null wheneversecurity.expression_languageis unavailable at runtime. Two ways to hit it:symfony/expression-languagenot installed — the case theisGranted()guard message already targets. Define a resource withsecurity: "is_granted(...)"and request it →usesObjectVariable()fatals before theisGranted()guard can report the missing library.Service pruned even when installed. The dependency is a soft reference:
In a kernel that doesn't register
access_control/ the Symfony expression voter,ResourceAccessCheckeris the only consumer ofsecurity.expression_language. Because the reference isnullOnInvalid(),RemoveUnusedDefinitionsPassremoves the private definition and the argument resolves tonull. Compile the container (e.g. prod) and request anysecurity:-protected operation → fatal inusesObjectVariable().Possible Solution
Mirror the existing
isGranted()guard inusesObjectVariable():Happy to open a PR.
Additional Context
Whether the service should be pruned in case (2) is arguably the consuming kernel's responsibility (it owns its container). This report is narrower: the asymmetric null handling inside
ResourceAccessCheckeris self-contained — the constructor permits null andisGranted()already anticipates it, sousesObjectVariable()should handle it the same way.Environment: Symfony security-core 8.0.12, PHP 8.4.