From cdfb174eb4f7065de693d71ea259b5a8503b8bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20REYNAUD?= Date: Fri, 22 May 2026 19:37:25 +0200 Subject: [PATCH] fix: Symfony CVE and possible ReDoS vulnerability --- composer.lock | 13 +++++----- src/Processor/StaticAssetProcessor.php | 36 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index 156b6d4..d1b6a9b 100644 --- a/composer.lock +++ b/composer.lock @@ -2768,16 +2768,16 @@ }, { "name": "symfony/runtime", - "version": "v7.4.8", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "6d792a64fec1eae2f011cfe9ab5978a9eab3071e" + "reference": "0b032fa77359745db793df5aff626779180c5f3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/6d792a64fec1eae2f011cfe9ab5978a9eab3071e", - "reference": "6d792a64fec1eae2f011cfe9ab5978a9eab3071e", + "url": "https://api.github.com/repos/symfony/runtime/zipball/0b032fa77359745db793df5aff626779180c5f3b", + "reference": "0b032fa77359745db793df5aff626779180c5f3b", "shasum": "" }, "require": { @@ -2790,6 +2790,7 @@ "require-dev": { "composer/composer": "^2.6", "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/dotenv": "^6.4|^7.0|^8.0", "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0" @@ -2827,7 +2828,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v7.4.8" + "source": "https://github.com/symfony/runtime/tree/v7.4.12" }, "funding": [ { @@ -2847,7 +2848,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/string", diff --git a/src/Processor/StaticAssetProcessor.php b/src/Processor/StaticAssetProcessor.php index 7bb2f02..716d9a2 100644 --- a/src/Processor/StaticAssetProcessor.php +++ b/src/Processor/StaticAssetProcessor.php @@ -39,9 +39,7 @@ public function process(string $path, string $extension): string $content = $minifier->minify(); $this->logger->info('Minified CSS: {path}', ['path' => $path]); } elseif (true === \in_array($extension, self::MINIFIABLE_JS, true)) { - $minifier = new Minify\JS($content); - $content = $minifier->minify(); - $this->logger->info('Minified JS: {path}', ['path' => $path]); + $content = $this->minifyJs($content, $path); } elseif (true === \in_array($extension, self::MINIFIABLE_JSON, true)) { $decoded = json_decode($content, true, 512, \JSON_THROW_ON_ERROR); $content = json_encode($decoded, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES); @@ -50,4 +48,36 @@ public function process(string $path, string $extension): string return $content; } + + /** + * matthiasmullie/minify uses recursive PCRE backreferences (?-2) in its for-loop patterns (JS.php lines 406-408), + * which can cause catastrophic backtracking on crafted input from an allowed domain (ReDoS). + * Tightening the PCRE limits before minification caps CPU consumption; on failure we serve the unminified content. + */ + private function minifyJs(string $content, string $path): string + { + $prevBacktrackLimit = (int) ini_get('pcre.backtrack_limit'); + $prevRecursionLimit = (int) ini_get('pcre.recursion_limit'); + ini_set('pcre.backtrack_limit', '100000'); + ini_set('pcre.recursion_limit', '500'); + + $minifier = new Minify\JS($content); + $minified = $minifier->minify(); + + ini_set('pcre.backtrack_limit', (string) $prevBacktrackLimit); + ini_set('pcre.recursion_limit', (string) $prevRecursionLimit); + + if (\PREG_NO_ERROR !== preg_last_error()) { + $this->logger->warning( + 'JS minification failed (PCRE error {error}), serving unminified: {path}', + ['error' => preg_last_error(), 'path' => $path], + ); + + return $content; + } + + $this->logger->info('Minified JS: {path}', ['path' => $path]); + + return $minified; + } }