From adb2937ddaf2d352cccbd5cd6d42cb9734af4da6 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 2 Jun 2026 12:17:02 +0200 Subject: [PATCH] fix(security): guard null EL in usesObjectVariable ResourceAccessChecker::usesObjectVariable() called $this->expressionLanguage->parse() without a null check, fataling with "Call to a member function parse() on null" when expression-language is missing or the service has been pruned by RemoveUnusedDefinitionsPass (the dependency is nullOnInvalid). Mirror the existing isGranted() guards: throw LogicException for both a null security stack (tokenStorage/authTrustResolver) and a null expressionLanguage. Closes #8215 --- .../Security/ResourceAccessChecker.php | 9 +++++++ .../Security/ResourceAccessCheckerTest.php | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Symfony/Security/ResourceAccessChecker.php b/src/Symfony/Security/ResourceAccessChecker.php index e12087a3a7e..657245fd693 100644 --- a/src/Symfony/Security/ResourceAccessChecker.php +++ b/src/Symfony/Security/ResourceAccessChecker.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Symfony\Security; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\ResourceAccessCheckerInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\ExpressionLanguage\Node\NameNode; @@ -50,6 +51,14 @@ public function isGranted(string $resourceClass, string $expression, array $extr public function usesObjectVariable(string $expression, array $variables = []): bool { + if (null === $this->tokenStorage || null === $this->authenticationTrustResolver) { + throw new RuntimeException('The "symfony/security" library must be installed to use the "security" attribute.'); + } + + if (null === $this->expressionLanguage) { + throw new RuntimeException('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()); } diff --git a/tests/Symfony/Security/ResourceAccessCheckerTest.php b/tests/Symfony/Security/ResourceAccessCheckerTest.php index aa124b14492..a8aa66f7e4e 100644 --- a/tests/Symfony/Security/ResourceAccessCheckerTest.php +++ b/tests/Symfony/Security/ResourceAccessCheckerTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Tests\Symfony\Security; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Symfony\Security\ResourceAccessChecker; use ApiPlatform\Tests\Fixtures\Serializable; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; @@ -82,6 +83,32 @@ public function testExpressionLanguageNotInstalled(): void $checker->isGranted(Dummy::class, 'is_granted("ROLE_ADMIN")'); } + public function testUsesObjectVariableThrowsWhenSecurityComponentNotAvailable(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The "symfony/security" library must be installed to use the "security" attribute.'); + + $checker = new ResourceAccessChecker($this->prophesize(ExpressionLanguage::class)->reveal()); + $checker->usesObjectVariable('user == object.owner'); + } + + public function testUsesObjectVariableThrowsWhenExpressionLanguageNotInstalled(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The "symfony/expression-language" library must be installed to use the "security" attribute.'); + + $authenticationTrustResolverProphecy = $this->prophesize(AuthenticationTrustResolverInterface::class); + $tokenStorageProphecy = $this->prophesize(TokenStorageInterface::class); + $tokenProphecy = $this->prophesize(TokenInterface::class); + $tokenProphecy->willImplement(Serializable::class); + $tokenProphecy->getUser()->willReturn(null); + $tokenProphecy->getRoleNames()->willReturn([]); + $tokenStorageProphecy->getToken()->willReturn($tokenProphecy->reveal()); + + $checker = new ResourceAccessChecker(null, $authenticationTrustResolverProphecy->reveal(), null, $tokenStorageProphecy->reveal()); + $checker->usesObjectVariable('user == object.owner'); + } + public function testWithoutAuthenticationToken(): void { $expressionLanguageProphecy = $this->prophesize(ExpressionLanguage::class);