diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f4197..477fd55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) where applicable. +## 1.1.10 — 2026-10-16 + +- Require Symfony YAML 8.1.6 or newer to exclude the inline lexer recursion issue CVE-2026-109413. +- Require Symfony HttpFoundation 8.1.9 or newer for the CVE-2026-108082 header parser fix and its whitespace-separator correction. +- Verify dependency version boundaries, bounded expression and YAML parsing, and header splitting without changing trusted configuration inputs. + ## 1.1.9 — 2026-10-15 - Require Symfony ExpressionLanguage 8.1.9 or newer to exclude releases affected by CVE-2026-109449. diff --git a/composer.json b/composer.json index 2b2f8e7..293bd22 100644 --- a/composer.json +++ b/composer.json @@ -24,13 +24,14 @@ "symfony/event-dispatcher": "^8.0", "symfony/expression-language": "^8.1.9", "symfony/filesystem": "^8.0", - "symfony/http-foundation": "^8.1", + "symfony/http-foundation": "^8.1.9", "symfony/http-kernel": "^8.1", "symfony/routing": "^8.1", "symfony/service-contracts": "^3.6", - "symfony/yaml": "^8.1" + "symfony/yaml": "^8.1.6" }, "require-dev": { + "composer/semver": "^3.4", "sympress/qa": "^0.1.2" }, "autoload": { diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md index 9dd30e7..c765b5b 100644 --- a/docs/dependency-injection.md +++ b/docs/dependency-injection.md @@ -21,7 +21,7 @@ build, configure, dump, and run the container: "symfony/expression-language": "^8.1.9", "symfony/filesystem": "^8.0", "symfony/service-contracts": "^3.6", - "symfony/yaml": "^8.1" + "symfony/yaml": "^8.1.6" } } ``` diff --git a/tests/Security/SymfonySecurityCompatibilityTest.php b/tests/Security/SymfonySecurityCompatibilityTest.php new file mode 100644 index 0000000..fec5c10 --- /dev/null +++ b/tests/Security/SymfonySecurityCompatibilityTest.php @@ -0,0 +1,88 @@ +} $manifest */ + $manifest = json_decode((string) file_get_contents(dirname(__DIR__, 2) . '/composer.json'), true, flags: JSON_THROW_ON_ERROR); + + self::assertFalse(Semver::satisfies($rejected, $manifest['require'][$package])); + self::assertTrue(Semver::satisfies($accepted, $manifest['require'][$package])); + } + + /** @return iterable */ + public static function dependencyBoundaries(): iterable + { + yield 'expression initial incomplete fix' => ['symfony/expression-language', '8.1.6', '8.1.9']; + yield 'expression last affected patch' => ['symfony/expression-language', '8.1.8', '8.1.9']; + yield 'yaml inline lexer' => ['symfony/yaml', '8.1.5', '8.1.6']; + yield 'header parser' => ['symfony/http-foundation', '8.1.6', '8.1.9']; + yield 'header whitespace separator regression' => ['symfony/http-foundation', '8.1.8', '8.1.9']; + } + + #[DataProvider('deepExpressions')] + public function testDependencyInjectionRejectsDeepExpressions(string $expression): void + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('maximum nesting level is 256'); + + (new ExpressionLanguage())->lint($expression, []); + } + + /** @return iterable */ + public static function deepExpressions(): iterable + { + yield 'unary nesting' => [str_repeat('!', 300) . '1']; + + $expression = '1'; + for ($level = 0; $level < 60; ++$level) { + $expression = '(' . $expression . str_repeat(' + 1', 60) . ')'; + } + yield 'chained binary nesting missed by 8.1.6' => [$expression]; + } + + public function testOrdinaryDependencyInjectionExpressionStillWorks(): void + { + self::assertSame(3, (new ExpressionLanguage())->evaluate('1 + 2')); + } + + public function testInlineYamlLexerHonorsConfiguredNestingLimit(): void + { + $parser = new Parser(8); + self::assertSame(['root' => [[1]]], $parser->parse('root: [[1]]')); + + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Maximum nesting depth of 8 exceeded'); + + $parser->parse('root: ' . str_repeat('[', 9)); + } + + public function testCacheControlDirectivesSurviveWhitespaceBeforeSeparator(): void + { + $request = new Request(server: ['HTTP_CACHE_CONTROL' => 'private' . str_repeat(' ', 32) . ', max-age=0']); + + self::assertTrue($request->headers->hasCacheControlDirective('private')); + self::assertSame('0', $request->headers->getCacheControlDirective('max-age')); + } + + public function testHeaderParserSupportsWhitespaceSeparators(): void + { + self::assertSame(['foo', 'bar'], HeaderUtils::split('foo bar', ' ')); + } +}