diff --git a/core/composer.lock b/core/composer.lock index 436b350130..683ba87df5 100644 --- a/core/composer.lock +++ b/core/composer.lock @@ -6389,16 +6389,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.21", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8" + "reference": "53d609da66578dad5a39fe290e8f924ec8f05e93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd0e3462be6d32a0d130e8423e4940a7688162d8", - "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/53d609da66578dad5a39fe290e8f924ec8f05e93", + "reference": "53d609da66578dad5a39fe290e8f924ec8f05e93", "shasum": "" }, "require": { @@ -6435,7 +6435,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.21" + "source": "https://github.com/symfony/filesystem/tree/v7.4.23" }, "funding": [ { @@ -6455,7 +6455,7 @@ "type": "tidelift" } ], - "time": "2026-10-06T20:05:19+00:00" + "time": "2026-11-04T08:27:22+00:00" }, { "name": "symfony/finder", @@ -6527,16 +6527,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.22", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "63eda1d0c4de229d6b3e825709e49dd5c149699a" + "reference": "7fb73cffa06e2bcc2cb523fcccb1e8174106b206" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/63eda1d0c4de229d6b3e825709e49dd5c149699a", - "reference": "63eda1d0c4de229d6b3e825709e49dd5c149699a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7fb73cffa06e2bcc2cb523fcccb1e8174106b206", + "reference": "7fb73cffa06e2bcc2cb523fcccb1e8174106b206", "shasum": "" }, "require": { @@ -6585,7 +6585,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.22" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.23" }, "funding": [ { @@ -6605,20 +6605,20 @@ "type": "tidelift" } ], - "time": "2026-10-27T13:26:03+00:00" + "time": "2026-11-04T08:27:22+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.22", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "87e727b33983238d3f2ea50f9f62265499ca1a6d" + "reference": "94c57f4c5c45a311d244a0aeadb72b14ee41d25d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/87e727b33983238d3f2ea50f9f62265499ca1a6d", - "reference": "87e727b33983238d3f2ea50f9f62265499ca1a6d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/94c57f4c5c45a311d244a0aeadb72b14ee41d25d", + "reference": "94c57f4c5c45a311d244a0aeadb72b14ee41d25d", "shasum": "" }, "require": { @@ -6704,7 +6704,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.22" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.23" }, "funding": [ { @@ -6724,7 +6724,7 @@ "type": "tidelift" } ], - "time": "2026-10-28T18:18:28+00:00" + "time": "2026-11-04T11:26:18+00:00" }, { "name": "symfony/mime", @@ -7547,16 +7547,16 @@ }, { "name": "symfony/routing", - "version": "v7.4.22", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "f79ed6fbf60d9e32d358054f1915295f3b8a6f7a" + "reference": "0000ade604b7bc798cb1e75edf6f17df48185f73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/f79ed6fbf60d9e32d358054f1915295f3b8a6f7a", - "reference": "f79ed6fbf60d9e32d358054f1915295f3b8a6f7a", + "url": "https://api.github.com/repos/symfony/routing/zipball/0000ade604b7bc798cb1e75edf6f17df48185f73", + "reference": "0000ade604b7bc798cb1e75edf6f17df48185f73", "shasum": "" }, "require": { @@ -7608,7 +7608,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.22" + "source": "https://github.com/symfony/routing/tree/v7.4.23" }, "funding": [ { @@ -7628,7 +7628,7 @@ "type": "tidelift" } ], - "time": "2026-10-26T21:52:50+00:00" + "time": "2026-11-04T08:27:22+00:00" }, { "name": "symfony/service-contracts", diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index 87f3e93136..eb3b2412f3 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -6668,17 +6668,17 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.21", - "version_normalized": "7.4.21.0", + "version": "v7.4.23", + "version_normalized": "7.4.23.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8" + "reference": "53d609da66578dad5a39fe290e8f924ec8f05e93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd0e3462be6d32a0d130e8423e4940a7688162d8", - "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/53d609da66578dad5a39fe290e8f924ec8f05e93", + "reference": "53d609da66578dad5a39fe290e8f924ec8f05e93", "shasum": "" }, "require": { @@ -6689,7 +6689,7 @@ "require-dev": { "symfony/process": "^6.4|^7.0|^8.0" }, - "time": "2026-10-06T20:05:19+00:00", + "time": "2026-11-04T08:27:22+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6717,7 +6717,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.21" + "source": "https://github.com/symfony/filesystem/tree/v7.4.23" }, "funding": [ { @@ -6812,17 +6812,17 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.22", - "version_normalized": "7.4.22.0", + "version": "v7.4.23", + "version_normalized": "7.4.23.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "63eda1d0c4de229d6b3e825709e49dd5c149699a" + "reference": "7fb73cffa06e2bcc2cb523fcccb1e8174106b206" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/63eda1d0c4de229d6b3e825709e49dd5c149699a", - "reference": "63eda1d0c4de229d6b3e825709e49dd5c149699a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7fb73cffa06e2bcc2cb523fcccb1e8174106b206", + "reference": "7fb73cffa06e2bcc2cb523fcccb1e8174106b206", "shasum": "" }, "require": { @@ -6845,7 +6845,7 @@ "symfony/mime": "^6.4|^7.0|^8.0", "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, - "time": "2026-10-27T13:26:03+00:00", + "time": "2026-11-04T08:27:22+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6873,7 +6873,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.22" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.23" }, "funding": [ { @@ -6897,17 +6897,17 @@ }, { "name": "symfony/http-kernel", - "version": "v7.4.22", - "version_normalized": "7.4.22.0", + "version": "v7.4.23", + "version_normalized": "7.4.23.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "87e727b33983238d3f2ea50f9f62265499ca1a6d" + "reference": "94c57f4c5c45a311d244a0aeadb72b14ee41d25d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/87e727b33983238d3f2ea50f9f62265499ca1a6d", - "reference": "87e727b33983238d3f2ea50f9f62265499ca1a6d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/94c57f4c5c45a311d244a0aeadb72b14ee41d25d", + "reference": "94c57f4c5c45a311d244a0aeadb72b14ee41d25d", "shasum": "" }, "require": { @@ -6967,7 +6967,7 @@ "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12|^4.0" }, - "time": "2026-10-28T18:18:28+00:00", + "time": "2026-11-04T11:26:18+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6995,7 +6995,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.22" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.23" }, "funding": [ { @@ -7868,17 +7868,17 @@ }, { "name": "symfony/routing", - "version": "v7.4.22", - "version_normalized": "7.4.22.0", + "version": "v7.4.23", + "version_normalized": "7.4.23.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "f79ed6fbf60d9e32d358054f1915295f3b8a6f7a" + "reference": "0000ade604b7bc798cb1e75edf6f17df48185f73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/f79ed6fbf60d9e32d358054f1915295f3b8a6f7a", - "reference": "f79ed6fbf60d9e32d358054f1915295f3b8a6f7a", + "url": "https://api.github.com/repos/symfony/routing/zipball/0000ade604b7bc798cb1e75edf6f17df48185f73", + "reference": "0000ade604b7bc798cb1e75edf6f17df48185f73", "shasum": "" }, "require": { @@ -7898,7 +7898,7 @@ "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/yaml": "^6.4|^7.0|^8.0" }, - "time": "2026-10-26T21:52:50+00:00", + "time": "2026-11-04T08:27:22+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -7932,7 +7932,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.22" + "source": "https://github.com/symfony/routing/tree/v7.4.23" }, "funding": [ { diff --git a/core/vendor/composer/installed.php b/core/vendor/composer/installed.php index 69b8a6b785..e2977ade9c 100644 --- a/core/vendor/composer/installed.php +++ b/core/vendor/composer/installed.php @@ -948,9 +948,9 @@ ), ), 'symfony/filesystem' => array( - 'pretty_version' => 'v7.4.21', - 'version' => '7.4.21.0', - 'reference' => 'fd0e3462be6d32a0d130e8423e4940a7688162d8', + 'pretty_version' => 'v7.4.23', + 'version' => '7.4.23.0', + 'reference' => '53d609da66578dad5a39fe290e8f924ec8f05e93', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/filesystem', 'aliases' => array(), @@ -966,18 +966,18 @@ 'dev_requirement' => false, ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v7.4.22', - 'version' => '7.4.22.0', - 'reference' => '63eda1d0c4de229d6b3e825709e49dd5c149699a', + 'pretty_version' => 'v7.4.23', + 'version' => '7.4.23.0', + 'reference' => '7fb73cffa06e2bcc2cb523fcccb1e8174106b206', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/http-kernel' => array( - 'pretty_version' => 'v7.4.22', - 'version' => '7.4.22.0', - 'reference' => '87e727b33983238d3f2ea50f9f62265499ca1a6d', + 'pretty_version' => 'v7.4.23', + 'version' => '7.4.23.0', + 'reference' => '94c57f4c5c45a311d244a0aeadb72b14ee41d25d', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-kernel', 'aliases' => array(), @@ -1104,9 +1104,9 @@ 'dev_requirement' => false, ), 'symfony/routing' => array( - 'pretty_version' => 'v7.4.22', - 'version' => '7.4.22.0', - 'reference' => 'f79ed6fbf60d9e32d358054f1915295f3b8a6f7a', + 'pretty_version' => 'v7.4.23', + 'version' => '7.4.23.0', + 'reference' => '0000ade604b7bc798cb1e75edf6f17df48185f73', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/routing', 'aliases' => array(), diff --git a/core/vendor/symfony/filesystem/Filesystem.php b/core/vendor/symfony/filesystem/Filesystem.php index fdd78abb06..0d711d14d7 100644 --- a/core/vendor/symfony/filesystem/Filesystem.php +++ b/core/vendor/symfony/filesystem/Filesystem.php @@ -163,35 +163,55 @@ private static function doRemove(array $files, bool $isRecursive): void throw new IOException(\sprintf('Failed to remove symlink "%s": ', $file).self::$lastError); } } elseif (is_dir($file)) { - if (!$isRecursive) { - $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!')); + $tmpDir = null; + $origFile = null; - if (file_exists($tmpName)) { - try { - self::doRemove([$tmpName], true); - } catch (IOException) { + try { + if (!$isRecursive && false !== $realPath = realpath($file)) { + for ($i = 0; $i < 10; ++$i) { + // Keep the temporary suffix within six characters to preserve room for descendant paths. + $tmpName = \dirname($realPath).'/.'.strtr(base64_encode(random_bytes(2)), ['/' => '-', '+' => '_', '=' => '']); + + if (!self::box('mkdir', $tmpName, 0o700)) { + continue; + } + + $tmpDir = $tmpName; + + if (self::box('rename', $file, $tmpDir.'/d')) { + $origFile = $file; + $file = $tmpDir.'/d'; + } + + break; } } - if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) { - $origFile = $file; - $file = $tmpName; - } else { - $origFile = null; - } - } + try { + $filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS); + } catch (\UnexpectedValueException $e) { + if (!file_exists($file)) { + continue; + } - $filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS); - self::doRemove(iterator_to_array($filesystemIterator, true), true); + throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).$e->getMessage(), 0, $e); + } - if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) { - $lastError = self::$lastError; + self::doRemove(iterator_to_array($filesystemIterator, true), true); - if (null !== $origFile && self::box('rename', $file, $origFile)) { - $file = $origFile; + if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) { + throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).self::$lastError); + } + } catch (IOException $e) { + if (null === $origFile || !self::box('rename', $file, $origFile)) { + throw $e; } - throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).$lastError); + throw new IOException(str_replace($file, $origFile, $e->getMessage()), 0, $e); + } finally { + if (null !== $tmpDir) { + self::box('rmdir', $tmpDir); + } } } elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) { throw new IOException(\sprintf('Failed to remove file "%s": ', $file).self::$lastError); diff --git a/core/vendor/symfony/filesystem/Path.php b/core/vendor/symfony/filesystem/Path.php index 47a92bcedf..8a116255cd 100644 --- a/core/vendor/symfony/filesystem/Path.php +++ b/core/vendor/symfony/filesystem/Path.php @@ -213,9 +213,9 @@ public static function getRoot(string $path): string } // Maintain scheme - if (false !== $schemeSeparatorPosition = strpos($path, '://')) { - $scheme = substr($path, 0, $schemeSeparatorPosition + 3); - $path = substr($path, $schemeSeparatorPosition + 3); + if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path, $m)) { + $scheme = $m[0]; + $path = substr($path, \strlen($scheme)); } else { $scheme = ''; } @@ -372,7 +372,7 @@ public static function isAbsolute(string $path): bool } // URLs and stream wrappers are considered absolute - if (str_contains($path, '://') && null !== parse_url($path, \PHP_URL_SCHEME)) { + if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path)) { return true; } @@ -460,9 +460,9 @@ public static function makeAbsolute(string $path, string $basePath): string return self::canonicalize($path); } - if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) { - $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3); - $basePath = substr($basePath, $schemeSeparatorPosition + 3); + if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $basePath, $m)) { + $scheme = $m[0]; + $basePath = substr($basePath, \strlen($scheme)); } else { $scheme = ''; } @@ -593,7 +593,7 @@ public static function makeRelative(string $path, string $basePath): string */ public static function isLocal(string $path): bool { - return '' !== $path && !str_contains($path, '://'); + return '' !== $path && !preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path); } /** @@ -795,9 +795,9 @@ private static function split(string $path): array } // Remember scheme as part of the root, if any - if (false !== $schemeSeparatorPosition = strpos($path, '://')) { - $root = substr($path, 0, $schemeSeparatorPosition + 3); - $path = substr($path, $schemeSeparatorPosition + 3); + if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path, $m)) { + $root = $m[0]; + $path = substr($path, \strlen($root)); } else { $root = ''; } diff --git a/core/vendor/symfony/http-foundation/BinaryFileResponse.php b/core/vendor/symfony/http-foundation/BinaryFileResponse.php index e16cd6b921..e51cd3d5b8 100644 --- a/core/vendor/symfony/http-foundation/BinaryFileResponse.php +++ b/core/vendor/symfony/http-foundation/BinaryFileResponse.php @@ -165,7 +165,7 @@ public function setContentDisposition(string $disposition, string $filename = '' $filename = $this->file->getFilename(); } - if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) { + if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/D', $filename) || str_contains($filename, '%'))) { $encoding = mb_detect_encoding($filename, null, true) ?: '8bit'; for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { diff --git a/core/vendor/symfony/http-foundation/HeaderUtils.php b/core/vendor/symfony/http-foundation/HeaderUtils.php index 290102e6d0..b039ac10dc 100644 --- a/core/vendor/symfony/http-foundation/HeaderUtils.php +++ b/core/vendor/symfony/http-foundation/HeaderUtils.php @@ -176,7 +176,7 @@ public static function makeDisposition(string $disposition, string $filename, st } // filenameFallback is not ASCII. - if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) { + if (!preg_match('/^[\x20-\x7e]*$/D', $filenameFallback)) { throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.'); } diff --git a/core/vendor/symfony/http-foundation/IpUtils.php b/core/vendor/symfony/http-foundation/IpUtils.php index 00d5233fbb..ff56dfd7c0 100644 --- a/core/vendor/symfony/http-foundation/IpUtils.php +++ b/core/vendor/symfony/http-foundation/IpUtils.php @@ -18,6 +18,13 @@ */ class IpUtils { + /** + * Ranges that a server must not connect to: the private networks, plus the documentation, benchmarking + * and other special-purpose ranges listed below. This is a deny list for outgoing requests, so it also + * covers the IPv6 prefixes that embed an IPv4 address (IPv4-mapped, IPv4-compatible, 6to4, Teredo, + * NAT64). A remote client can present such an address as its own, so this list must not serve as an + * allow list of trusted proxies: use PRIVATE_RANGES for that. + */ public const PRIVATE_SUBNETS = [ '127.0.0.0/8', // RFC1700 (Loopback) '10.0.0.0/8', // RFC1918 @@ -45,6 +52,25 @@ class IpUtils '64:ff9b:1::/48', // NAT64 local-use prefix (RFC 8215) ]; + /** + * Ranges reserved for private networks, which a reverse proxy in front of the application can live in. + * + * Unlike PRIVATE_SUBNETS, this list holds no prefix that a remote client can present as its own address, + * so it can serve as an allow list, as Request::setTrustedProxies() does for "private_ranges". + * An IPv4-mapped IPv6 address is matched as the IPv4 host it embeds. + */ + public const PRIVATE_RANGES = [ + '127.0.0.0/8', // RFC1700 (Loopback) + '10.0.0.0/8', // RFC1918 + '172.16.0.0/12', // RFC1918 + '192.168.0.0/16', // RFC1918 + '169.254.0.0/16', // RFC3927 + '100.64.0.0/10', // RFC6598 + '::1/128', // Loopback + 'fc00::/7', // Unique Local Address + 'fe80::/10', // Link Local Address + ]; + private static array $checkedIps = []; /** @@ -66,11 +92,21 @@ public static function checkIp(string $requestIp, string|array $ips): bool } $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; + $embeddedIp4 = null; + + // An IPv4-mapped IPv6 address is the address of an IPv4 host, so it is also checked against IPv4 subnets as that host + if ('checkIp6' === $method && filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) && false !== ($packed = @inet_pton($requestIp)) && 16 === \strlen($packed) && "\0\0\0\0\0\0\0\0\0\0\xFF\xFF" === substr($packed, 0, 12)) { + $embeddedIp4 = inet_ntop(substr($packed, 12)); + } foreach ($ips as $ip) { if (self::$method($requestIp, $ip)) { return true; } + + if (null !== $embeddedIp4 && !str_contains($ip, ':') && self::checkIp4($embeddedIp4, $ip)) { + return true; + } } return false; diff --git a/core/vendor/symfony/http-foundation/Request.php b/core/vendor/symfony/http-foundation/Request.php index a0366a8a0e..e501d8791b 100644 --- a/core/vendor/symfony/http-foundation/Request.php +++ b/core/vendor/symfony/http-foundation/Request.php @@ -611,7 +611,7 @@ public function overrideGlobals(): void * * You should only list the reverse proxies that you manage directly. * - * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] and 'PRIVATE_SUBNETS' by IpUtils::PRIVATE_SUBNETS + * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'], and 'private_ranges' or 'PRIVATE_SUBNETS' by IpUtils::PRIVATE_RANGES * @param int-mask-of $trustedHeaderSet A bit field to set which headers to trust from your proxies */ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet): void @@ -627,7 +627,7 @@ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet): if (false !== ($i = array_search('PRIVATE_SUBNETS', $proxies, true)) || false !== ($i = array_search('private_ranges', $proxies, true))) { unset($proxies[$i]); - $proxies = array_merge($proxies, IpUtils::PRIVATE_SUBNETS); + $proxies = array_merge($proxies, IpUtils::PRIVATE_RANGES); } self::$trustedProxies = $proxies; @@ -1973,8 +1973,13 @@ protected function prepareBaseUrl(): string $requestUri = '/'.$requestUri; } - if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { - // full $baseUrl matches + if (str_starts_with($requestUri, '//') || str_starts_with($requestUri, '/\\')) { + // the URI starts with a host that the client chose: URLs built on it would point there + return ''; + } + + if ($baseUrl && (null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) && \in_array($requestUri[\strlen($prefix)] ?? '', ['', '/', '?'], true)) { + // full $baseUrl matches, and is not only the start of a longer path segment return $prefix; } @@ -1988,17 +1993,19 @@ protected function prepareBaseUrl(): string $truncatedRequestUri = substr($requestUri, 0, $pos); } - $basename = basename($baseUrl ?? ''); - if (!$basename || !strpos(rawurldecode($truncatedRequestUri), $basename)) { - // no match whatsoever; set it blank + // With mod_rewrite or ISAPI_Rewrite, the script filename can come after + // a prefix in the path: the base URL then ends with the script filename + $decodedRequestUri = rawurldecode($truncatedRequestUri); + if (!$baseUrl || !$pos = ($this->isIisRewrite() ? stripos($decodedRequestUri, $baseUrl) : strpos($decodedRequestUri, $baseUrl))) { + // the script filename isn't in the path, or only at its start, which is handled above return ''; } - // If using mod_rewrite or ISAPI_Rewrite strip the script filename - // out of baseUrl. $pos !== 0 makes sure it is not matching a value - // from PATH_INFO or QUERY_STRING - if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) { - $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl)); + $baseUrl = $this->getUrlencodedPrefix($truncatedRequestUri, substr($decodedRequestUri, 0, $pos + \strlen($baseUrl))) ?? ''; + + if (!\in_array($requestUri[\strlen($baseUrl)] ?? '', ['', '/', '?'], true)) { + // the script filename is only the start of a longer path segment + return ''; } return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR); @@ -2114,13 +2121,12 @@ private function getUrlencodedPrefix(string $string, string $prefix): ?string return null; } + // Each byte of the prefix takes 1 or 3 characters in the string. + // A {n} quantifier would make PCRE fail to compile for a long prefix. $len = \strlen($prefix); + preg_match_all('#%[[:xdigit:]]{2}|.#s', substr($string, 0, 3 * $len), $match); - if (preg_match(\sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { - return $match[0]; - } - - return null; + return implode('', \array_slice($match[0], 0, $len)); } private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): static diff --git a/core/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php b/core/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php index b37f6e3c87..2c09c3abc6 100644 --- a/core/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php +++ b/core/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php @@ -33,6 +33,11 @@ class MethodRequestMatcher implements RequestMatcherInterface public function __construct(array|string $methods) { $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static fn (array $methods, string $method) => array_merge($methods, preg_split('/\s*,\s*/', $method)), []); + + // HEAD requests are served by the GET handler, as the router does + if (\in_array('GET', $this->methods, true) && !\in_array('HEAD', $this->methods, true)) { + $this->methods[] = 'HEAD'; + } } public function matches(Request $request): bool diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php index 25b1eb5ea7..8f41abcfb3 100644 --- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -23,7 +23,7 @@ */ class SessionHandlerFactory { - public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler + public static function createHandler(#[\SensitiveParameter] object|string $connection, #[\SensitiveParameter] array $options = []): AbstractSessionHandler { if ($query = \is_string($connection) ? parse_url($connection) : false) { parse_str($query['query'] ?? '', $query); @@ -93,6 +93,6 @@ public static function createHandler(object|string $connection, array $options = return new PdoSessionHandler($connection, $options); } - throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', $connection)); + throw new \InvalidArgumentException('No session handler supports the given DSN.'); } } diff --git a/core/vendor/symfony/http-foundation/UrlHelper.php b/core/vendor/symfony/http-foundation/UrlHelper.php index 22fb9a1a1b..c442fad2bf 100644 --- a/core/vendor/symfony/http-foundation/UrlHelper.php +++ b/core/vendor/symfony/http-foundation/UrlHelper.php @@ -37,10 +37,20 @@ public function getAbsoluteUrl(string $path): string return $this->getAbsoluteUrlFromContext($path); } - if ('#' === $path[0]) { - $path = $request->getRequestUri().$path; - } elseif ('?' === $path[0]) { - $path = $request->getPathInfo().$path; + if ('' === $path || '#' === $path[0] || '?' === $path[0]) { + [$requestPath, $queryString] = explode('?', $request->getRequestUri(), 2) + [1 => null]; + $pathInfo = $request->getPathInfo(); + + // the path info of "/index.php" is "/", but the URL has no slash after the base URL + if (!str_ends_with($requestPath, $pathInfo)) { + $pathInfo = substr($pathInfo, 1); + } + + if (null !== $queryString && !str_starts_with($path, '?')) { + $path = '?'.$queryString.$path; + } + + $path = $request->getBaseUrl().$pathInfo.$path; } if (!$path || '/' !== $path[0]) { @@ -92,7 +102,9 @@ private function getAbsoluteUrlFromContext(string $path): string $port = ':'.$context->getHttpsPort(); } - if ('#' === $path[0]) { + $isCurrentPathReference = '' === $path || str_starts_with($path, '#') || str_starts_with($path, '?'); + + if ('' === $path || '#' === $path[0]) { $queryString = $context->getQueryString(); $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { @@ -101,6 +113,8 @@ private function getAbsoluteUrlFromContext(string $path): string if ('/' !== $path[0]) { $path = rtrim($context->getBaseUrl(), '/').'/'.$path; + } elseif ($isCurrentPathReference) { + $path = $context->getBaseUrl().$path; } return $scheme.'://'.$host.$port.$path; diff --git a/core/vendor/symfony/http-kernel/Attribute/MapRequestPayload.php b/core/vendor/symfony/http-kernel/Attribute/MapRequestPayload.php index cf086380c0..138dc3a7e5 100644 --- a/core/vendor/symfony/http-kernel/Attribute/MapRequestPayload.php +++ b/core/vendor/symfony/http-kernel/Attribute/MapRequestPayload.php @@ -27,7 +27,7 @@ class MapRequestPayload extends ValueResolver public ArgumentMetadata $metadata; /** - * @param array|string|null $acceptFormat The payload formats to accept (i.e. "json", "xml") + * @param array|string|null $acceptFormat The payload formats to accept (i.e. "json", "xml"); formats other than "form", "json" and "xml" are accepted only when listed here * @param array $serializationContext The serialization context to use when deserializing the payload * @param string|GroupSequence|array|null $validationGroups The validation groups to use when validating the query string mapping * @param class-string $resolver The class name of the resolver to use diff --git a/core/vendor/symfony/http-kernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/core/vendor/symfony/http-kernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 6dca19787b..bccf86b935 100644 --- a/core/vendor/symfony/http-kernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/core/vendor/symfony/http-kernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -220,6 +220,10 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument, throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); } + if (!$attribute->acceptFormat && !\in_array($format, ['form', 'json', 'xml'], true)) { + throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format, "%s" must be explicitly listed in "acceptFormat".', $format)); + } + if ('array' === $argument->getType() && null !== $attribute->type) { $type = $attribute->type.'[]'; } else { diff --git a/core/vendor/symfony/http-kernel/EventListener/CacheAttributeListener.php b/core/vendor/symfony/http-kernel/EventListener/CacheAttributeListener.php index 0046e4a7be..99d2e4d7d2 100644 --- a/core/vendor/symfony/http-kernel/EventListener/CacheAttributeListener.php +++ b/core/vendor/symfony/http-kernel/EventListener/CacheAttributeListener.php @@ -215,7 +215,9 @@ private function toSeconds(int|string $time): int { if (!is_numeric($time)) { $now = time(); - $time = strtotime($time, $now) - $now; + $then = strtotime($time, $now); + // ignore daylight saving time changes, so that "1 day" is always 86400 seconds + $time = $then - $now + idate('Z', $then) - idate('Z', $now); } return $time; diff --git a/core/vendor/symfony/http-kernel/EventListener/RouterListener.php b/core/vendor/symfony/http-kernel/EventListener/RouterListener.php index 97c825dfab..31b5a56778 100644 --- a/core/vendor/symfony/http-kernel/EventListener/RouterListener.php +++ b/core/vendor/symfony/http-kernel/EventListener/RouterListener.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -42,6 +43,7 @@ class RouterListener implements EventSubscriberInterface { private RequestContext $context; + private array $parentContexts = []; /** * @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface) @@ -78,15 +80,44 @@ private function setCurrentRequest(?Request $request): void * After a sub-request is done, we need to reset the routing context to the parent request so that the URL generator * operates on the correct context again. */ - public function onKernelFinishRequest(): void + public function onKernelFinishRequest(?FinishRequestEvent $event = null): void { - $this->setCurrentRequest($this->requestStack->getParentRequest()); + if (null === $event) { + $this->setCurrentRequest($this->requestStack->getParentRequest()); + + return; + } + + $id = spl_object_id($event->getRequest()); + + if (!isset($this->parentContexts[$id])) { + return; + } + + // The context of the parent is restored instead of being computed again: + // while a sub-request runs, its trusted proxies also apply to the parent request + $parentContext = $this->parentContexts[$id]; + unset($this->parentContexts[$id]); + + $this->context + ->setBaseUrl($parentContext->getBaseUrl()) + ->setPathInfo($parentContext->getPathInfo()) + ->setMethod($parentContext->getMethod()) + ->setHost($parentContext->getHost()) + ->setScheme($parentContext->getScheme()) + ->setHttpPort($parentContext->getHttpPort()) + ->setHttpsPort($parentContext->getHttpsPort()) + ->setQueryString($parentContext->getQueryString()); } public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); + if (null !== $this->requestStack->getParentRequest()) { + $this->parentContexts[spl_object_id($request)] = clone $this->context; + } + $this->setCurrentRequest($request); if ($request->attributes->has('_controller')) { diff --git a/core/vendor/symfony/http-kernel/Kernel.php b/core/vendor/symfony/http-kernel/Kernel.php index 1d3b9552c4..a0848daaad 100644 --- a/core/vendor/symfony/http-kernel/Kernel.php +++ b/core/vendor/symfony/http-kernel/Kernel.php @@ -74,11 +74,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.4.22'; - public const VERSION_ID = 70422; + public const VERSION = '7.4.23'; + public const VERSION_ID = 70423; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 22; + public const RELEASE_VERSION = 23; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2028'; diff --git a/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php b/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php index 5407e129f8..00bebe23ad 100644 --- a/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php +++ b/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php @@ -78,7 +78,7 @@ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?i $urlFilter = str_starts_with($url, '!') ? str_contains($csvUrl, substr($url, 1)) : !str_contains($csvUrl, $url); } - if ($ip && !str_contains($csvIp, $ip) || $urlFilter || $method && !str_contains($csvMethod, $method) || $statusCode && !str_contains($csvStatusCode, $statusCode)) { + if ($ip && !str_contains($csvIp, $ip) || $urlFilter || $method && !str_contains($csvMethod, $method) || null !== $statusCode && '' !== $statusCode && !str_contains($csvStatusCode, $statusCode)) { continue; } diff --git a/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php index d79e1ba2df..b40958942a 100644 --- a/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -315,6 +315,17 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo $state->vars = []; $hostRegex = '(?i:'.preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]).')\.'; $state->hostVars = $state->vars; + + if ($state->hostVars) { + // Group 1 captures the path so that host variables cannot match any part of it. + // Group 1 is mapped to the first host variable, then the group of that variable overwrites it. + // The first lookahead skips the capture when the host cannot match. + // It is left out when requirements contain named groups, because a regexp can't define the same name twice. + $hostCheck = preg_replace('#\?P<[^>]++>#', '?:', $rx[1]); + $hostCheck = preg_match('#\(\?(?:<[^=!]|\')#', $hostCheck) ? '' : '(?=(?i:'.$hostCheck.')\.)'; + $hostRegex = $hostCheck.'(?=[^/]*+(.*+))'.$hostRegex.'(?=\1)'; + array_unshift($state->hostVars, $state->hostVars[0]); + } } else { $hostRegex = '(?:(?:[^./]*+\.)++)'; $state->hostVars = [];