Skip to content

[Security] ResourceAccessChecker::usesObjectVariable() fatals on a null ExpressionLanguage (inconsistent with isGranted()) #8215

@hirale

Description

@hirale

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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions