--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:php-guzzlehttp-psr7
User: [email protected]
Usertags: pu
[ This issue is similar to #1139278 for Trixie. ]
Hi,
As agreed with the security team, I’dd like to address CVE-2026-48998
and CVE-2026-49214 in the next point release since they don’t deserve a
DSA. Please note that I had to include three more upstream patches in
order to properly include the fix for CVE-2026-48998.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
Thanks in advance for considering.
Regards,
taffit
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/changelog php-guzzlehttp-psr7-2.4.5/debian/changelog
--- php-guzzlehttp-psr7-2.4.5/debian/changelog 2023-04-22 10:56:19.000000000 +0200
+++ php-guzzlehttp-psr7-2.4.5/debian/changelog 2026-05-30 16:57:02.000000000 +0200
@@ -1,3 +1,15 @@
+php-guzzlehttp-psr7 (2.4.5-1+deb12u1) bookworm; urgency=medium
+
+ * Backport fixes from upstream
+ - Encode plus sign in withQueryValue() and withQueryValues() (#636)
+ - Harden ServerRequest globals handling (#660)
+ - Normalize global header values (#718)
+ - Reject control characters in URI hosts (#715) [CVE-2026-49214]
+ - Reject malformed Host authorities (#717) [CVE-2026-48998]
+ (Closes: #1138265)
+
+ -- David Prévot <[email protected]> Sat, 30 May 2026 16:57:02 +0200
+
php-guzzlehttp-psr7 (2.4.5-1) unstable; urgency=medium
[ Graham Campbell ]
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/patches/0002-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch php-guzzlehttp-psr7-2.4.5/debian/patches/0002-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch
--- php-guzzlehttp-psr7-2.4.5/debian/patches/0002-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-guzzlehttp-psr7-2.4.5/debian/patches/0002-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch 2026-05-30 16:55:40.000000000 +0200
@@ -0,0 +1,61 @@
+From: =?utf-8?q?Ey=C3=BCp_Can_Akman?= <[email protected]>
+Date: Tue, 10 Mar 2026 12:53:17 +0300
+Subject: [2.8] Encode plus sign in withQueryValue() and withQueryValues()
+ (#636)
+
+* Encode plus sign in withQueryValue() and withQueryValues()
+
+* Add plus sign note to Uri query comment
+
+Origin: upstrem, https://github.com/guzzle/psr7/commit/bf5b5784ad30e4d01d7723366977ae54dc1d06ff
+Bug: https://github.com/guzzle/psr7/pull/636
+---
+ src/Uri.php | 5 +++--
+ tests/UriTest.php | 11 +++++++++++
+ 2 files changed, 14 insertions(+), 2 deletions(-)
+
+diff --git a/src/Uri.php b/src/Uri.php
+index 09e878d..8292a04 100644
+--- a/src/Uri.php
++++ b/src/Uri.php
+@@ -51,7 +51,7 @@ class Uri implements UriInterface, \JsonSerializable
+ * @link https://tools.ietf.org/html/rfc3986#section-2.2
+ */
+ private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
+- private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26'];
++ private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
+
+ /** @var string Uri scheme. */
+ private $scheme = '';
+@@ -658,7 +658,8 @@ class Uri implements UriInterface, \JsonSerializable
+
+ private static function generateQueryString(string $key, ?string $value): string
+ {
+- // Query string separators ("=", "&") within the key or value need to be encoded
++ // Query string separators ("=", "&") and literal plus signs ("+") within the
++ // key or value need to be encoded
+ // (while preventing double-encoding) before setting the query string. All other
+ // chars that need percent-encoding will be encoded by withQuery().
+ $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
+diff --git a/tests/UriTest.php b/tests/UriTest.php
+index 6484a24..c807807 100644
+--- a/tests/UriTest.php
++++ b/tests/UriTest.php
+@@ -388,6 +388,17 @@ class UriTest extends TestCase
+ self::assertSame('E%3Dmc%5e2=ein%26stein', $uri->getQuery(), 'Encoded key/value do not get double-encoded');
+ }
+
++ public function testWithQueryValueEncodesPlusSign(): void
++ {
++ $uri = new Uri();
++ $uri = Uri::withQueryValue($uri, 'a+b', 'c+d');
++ self::assertSame('a%2Bb=c%2Bd', $uri->getQuery(), 'Plus signs in key and value get encoded to %2B');
++
++ $uri = new Uri();
++ $uri = Uri::withQueryValue($uri, 'query', 'a+b c');
++ self::assertSame('query=a%2Bb%20c', $uri->getQuery(), 'Plus sign is encoded distinctly from space');
++ }
++
+ public function testWithoutQueryValueHandlesEncoding(): void
+ {
+ // It also tests that the case of the percent-encoding does not matter,
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/patches/0003-Harden-ServerRequest-globals-handling-660.patch php-guzzlehttp-psr7-2.4.5/debian/patches/0003-Harden-ServerRequest-globals-handling-660.patch
--- php-guzzlehttp-psr7-2.4.5/debian/patches/0003-Harden-ServerRequest-globals-handling-660.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-guzzlehttp-psr7-2.4.5/debian/patches/0003-Harden-ServerRequest-globals-handling-660.patch 2026-05-30 16:55:40.000000000 +0200
@@ -0,0 +1,181 @@
+From: Graham Campbell <[email protected]>
+Date: Tue, 19 May 2026 18:20:12 +0100
+Subject: Harden ServerRequest globals handling (#660)
+
+* Harden server request globals
+
+* Document ServerRequest globals hardening
+
+* Move ServerRequest changelog entry
+
+* Reorder ServerRequest changelog entry
+
+Origin: backport, https://github.com/guzzle/psr7/commit/ed659d5c2254b9d2af3a1b77257b91a4899e313c
+Bug: https://github.com/guzzle/psr7/pull/660
+---
+ src/ServerRequest.php | 46 +++++++++++++++++++++++++++++----------------
+ tests/ServerRequestTest.php | 41 ++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 71 insertions(+), 16 deletions(-)
+
+diff --git a/src/ServerRequest.php b/src/ServerRequest.php
+index b2aa382..8b236a4 100644
+--- a/src/ServerRequest.php
++++ b/src/ServerRequest.php
+@@ -165,11 +165,12 @@ class ServerRequest extends Request implements ServerRequestInterface
+ */
+ public static function fromGlobals(): ServerRequestInterface
+ {
+- $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
++ $method = self::getServerParam('REQUEST_METHOD') ?? 'GET';
+ $headers = getallheaders();
+ $uri = self::getUriFromGlobals();
+ $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
+- $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1';
++ $serverProtocol = self::getServerParam('SERVER_PROTOCOL');
++ $protocol = $serverProtocol !== null ? str_replace('HTTP/', '', $serverProtocol) : '1.1';
+
+ $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
+
+@@ -180,11 +181,19 @@ class ServerRequest extends Request implements ServerRequestInterface
+ ->withUploadedFiles(self::normalizeFiles($_FILES));
+ }
+
++ private static function getServerParam(string $key): ?string
++ {
++ return isset($_SERVER[$key]) && is_string($_SERVER[$key]) ? $_SERVER[$key] : null;
++ }
++
++ /**
++ * @return array{0: string|null, 1: int|null}
++ */
+ private static function extractHostAndPortFromAuthority(string $authority): array
+ {
+ $uri = 'http://' . $authority;
+ $parts = parse_url($uri);
+- if (false === $parts) {
++ if (!is_array($parts)) {
+ return [null, null];
+ }
+
+@@ -201,11 +210,13 @@ class ServerRequest extends Request implements ServerRequestInterface
+ {
+ $uri = new Uri('');
+
+- $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
++ $https = self::getServerParam('HTTPS');
++ $uri = $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
+
+ $hasPort = false;
+- if (isset($_SERVER['HTTP_HOST'])) {
+- [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']);
++ $authority = self::getServerParam('HTTP_HOST');
++ if ($authority !== null) {
++ [$host, $port] = self::extractHostAndPortFromAuthority($authority);
+ if ($host !== null) {
+ $uri = $uri->withHost($host);
+ }
+@@ -214,19 +225,21 @@ class ServerRequest extends Request implements ServerRequestInterface
+ $hasPort = true;
+ $uri = $uri->withPort($port);
+ }
+- } elseif (isset($_SERVER['SERVER_NAME'])) {
+- $uri = $uri->withHost($_SERVER['SERVER_NAME']);
+- } elseif (isset($_SERVER['SERVER_ADDR'])) {
+- $uri = $uri->withHost($_SERVER['SERVER_ADDR']);
++ } elseif (($serverName = self::getServerParam('SERVER_NAME')) !== null) {
++ $uri = $uri->withHost($serverName);
++ } elseif (($serverAddr = self::getServerParam('SERVER_ADDR')) !== null) {
++ $uri = $uri->withHost($serverAddr);
+ }
+
+- if (!$hasPort && isset($_SERVER['SERVER_PORT'])) {
+- $uri = $uri->withPort($_SERVER['SERVER_PORT']);
++ $serverPort = self::getServerParam('SERVER_PORT');
++ if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/', $serverPort) === 1) {
++ $uri = $uri->withPort((int) $serverPort);
+ }
+
+ $hasQuery = false;
+- if (isset($_SERVER['REQUEST_URI'])) {
+- $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2);
++ $requestUri = self::getServerParam('REQUEST_URI');
++ if ($requestUri !== null) {
++ $requestUriParts = explode('?', $requestUri, 2);
+ $uri = $uri->withPath($requestUriParts[0]);
+ if (isset($requestUriParts[1])) {
+ $hasQuery = true;
+@@ -234,8 +247,9 @@ class ServerRequest extends Request implements ServerRequestInterface
+ }
+ }
+
+- if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) {
+- $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
++ $queryString = self::getServerParam('QUERY_STRING');
++ if (!$hasQuery && $queryString !== null) {
++ $uri = $uri->withQuery($queryString);
+ }
+
+ return $uri;
+diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php
+index 15340f2..dba10e7 100644
+--- a/tests/ServerRequestTest.php
++++ b/tests/ServerRequestTest.php
+@@ -358,10 +358,34 @@ class ServerRequestTest extends TestCase
+ 'https://www.example.org:8324/blog/article.php?id=10&user=foo',
+ array_merge($server, ['SERVER_PORT' => '8324']),
+ ],
++ 'Invalid SERVER_PORT is ignored instead of coerced to zero' => [
++ 'https://www.example.org/blog/article.php?id=10&user=foo',
++ array_merge($server, ['SERVER_PORT' => 'not-a-port']),
++ ],
++ 'Non-string SERVER_PORT is ignored' => [
++ 'https://www.example.org/blog/article.php?id=10&user=foo',
++ array_merge($server, ['SERVER_PORT' => ['443']]),
++ ],
++ 'Non-string HTTP_HOST falls back to SERVER_NAME' => [
++ 'https://www.example.org/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => ['www.example.org']]),
++ ],
++ 'Non-string SERVER_NAME falls back to SERVER_ADDR' => [
++ 'https://217.112.82.20/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => null, 'SERVER_NAME' => ['www.example.org']]),
++ ],
+ 'REQUEST_URI missing query string' => [
+ 'https://www.example.org/blog/article.php?id=10&user=foo',
+ array_merge($server, ['REQUEST_URI' => '/blog/article.php']),
+ ],
++ 'Non-string REQUEST_URI is treated as missing' => [
++ 'https://www.example.org?id=10&user=foo',
++ array_merge($server, ['REQUEST_URI' => ['bad']]),
++ ],
++ 'Non-string QUERY_STRING is treated as missing' => [
++ 'https://www.example.org/blog/article.php',
++ array_merge($server, ['REQUEST_URI' => '/blog/article.php', 'QUERY_STRING' => ['bad']]),
++ ],
+ 'Empty server variable' => [
+ 'http://localhost',
+ [],
+@@ -461,6 +485,23 @@ class ServerRequestTest extends TestCase
+ self::assertEquals($expectedFiles, $server->getUploadedFiles());
+ }
+
++ public function testFromGlobalsDefaultsNonStringMethodAndProtocol(): void
++ {
++ $_SERVER = [
++ 'REQUEST_METHOD' => ['POST'],
++ 'SERVER_PROTOCOL' => ['HTTP/1.1'],
++ 'REQUEST_URI' => '/',
++ 'SERVER_PORT' => '80',
++ ];
++
++ $_COOKIE = $_POST = $_GET = $_FILES = [];
++
++ $server = ServerRequest::fromGlobals();
++
++ self::assertSame('GET', $server->getMethod());
++ self::assertSame('1.1', $server->getProtocolVersion());
++ }
++
+ public function testUploadedFiles(): void
+ {
+ $request1 = new ServerRequest('GET', '/');
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/patches/0004-Normalize-global-header-values-718.patch php-guzzlehttp-psr7-2.4.5/debian/patches/0004-Normalize-global-header-values-718.patch
--- php-guzzlehttp-psr7-2.4.5/debian/patches/0004-Normalize-global-header-values-718.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-guzzlehttp-psr7-2.4.5/debian/patches/0004-Normalize-global-header-values-718.patch 2026-05-30 16:55:40.000000000 +0200
@@ -0,0 +1,102 @@
+From: Graham Campbell <[email protected]>
+Date: Mon, 25 May 2026 21:50:08 +0100
+Subject: Normalize global header values (#718)
+
+Origin: backport, https://github.com/guzzle/psr7/commit/a0fda818b0f74482925e66e814fe9afb48fd2fa5
+Bug: https://github.com/guzzle/psr7/pull/718
+---
+ src/ServerRequest.php | 28 +++++++++++++++++++++++++++-
+ tests/ServerRequestTest.php | 34 ++++++++++++++++++++++++++++++++++
+ 2 files changed, 61 insertions(+), 1 deletion(-)
+
+diff --git a/src/ServerRequest.php b/src/ServerRequest.php
+index 8b236a4..66bfa96 100644
+--- a/src/ServerRequest.php
++++ b/src/ServerRequest.php
+@@ -166,7 +166,7 @@ class ServerRequest extends Request implements ServerRequestInterface
+ public static function fromGlobals(): ServerRequestInterface
+ {
+ $method = self::getServerParam('REQUEST_METHOD') ?? 'GET';
+- $headers = getallheaders();
++ $headers = self::getAllHeaders();
+ $uri = self::getUriFromGlobals();
+ $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
+ $serverProtocol = self::getServerParam('SERVER_PROTOCOL');
+@@ -181,6 +181,32 @@ class ServerRequest extends Request implements ServerRequestInterface
+ ->withUploadedFiles(self::normalizeFiles($_FILES));
+ }
+
++ /**
++ * @return array<array-key, string>
++ */
++ private static function getAllHeaders(): array
++ {
++ return self::normalizeHeaderValues(getallheaders());
++ }
++
++ /**
++ * @param array<array-key, mixed> $headers
++ *
++ * @return array<array-key, string>
++ */
++ private static function normalizeHeaderValues(array $headers): array
++ {
++ $normalized = [];
++
++ foreach ($headers as $name => $value) {
++ if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
++ $normalized[$name] = (string) $value;
++ }
++ }
++
++ return $normalized;
++ }
++
+ private static function getServerParam(string $key): ?string
+ {
+ return isset($_SERVER[$key]) && is_string($_SERVER[$key]) ? $_SERVER[$key] : null;
+diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php
+index dba10e7..c07503e 100644
+--- a/tests/ServerRequestTest.php
++++ b/tests/ServerRequestTest.php
+@@ -485,6 +485,40 @@ class ServerRequestTest extends TestCase
+ self::assertEquals($expectedFiles, $server->getUploadedFiles());
+ }
+
++ public function testFromGlobalsNormalizesUnexpectedHeaderValueTypes(): void
++ {
++ $_SERVER = [
++ 'REQUEST_URI' => '/',
++ 'HTTP_HOST' => 'www.example.org',
++ 'HTTP_X_INT' => 123,
++ 'HTTP_X_FLOAT' => 1.5,
++ 'HTTP_X_FALSE' => false,
++ 'HTTP_X_TRUE' => true,
++ 'HTTP_X_STRINGABLE' => new class {
++ public function __toString(): string
++ {
++ return 'stringable';
++ }
++ },
++ 'HTTP_X_ARRAY' => ['bad'],
++ 'HTTP_X_OBJECT' => new \stdClass(),
++ 'HTTP_123' => 'numeric header',
++ ];
++
++ $_COOKIE = $_POST = $_GET = $_FILES = [];
++
++ $server = ServerRequest::fromGlobals();
++
++ self::assertSame('123', $server->getHeaderLine('X-Int'));
++ self::assertSame('1.5', $server->getHeaderLine('X-Float'));
++ self::assertSame([''], $server->getHeader('X-False'));
++ self::assertSame('1', $server->getHeaderLine('X-True'));
++ self::assertSame('stringable', $server->getHeaderLine('X-Stringable'));
++ self::assertSame('numeric header', $server->getHeaderLine('123'));
++ self::assertFalse($server->hasHeader('X-Array'));
++ self::assertFalse($server->hasHeader('X-Object'));
++ }
++
+ public function testFromGlobalsDefaultsNonStringMethodAndProtocol(): void
+ {
+ $_SERVER = [
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/patches/0005-Reject-malformed-Host-authorities-717.patch php-guzzlehttp-psr7-2.4.5/debian/patches/0005-Reject-malformed-Host-authorities-717.patch
--- php-guzzlehttp-psr7-2.4.5/debian/patches/0005-Reject-malformed-Host-authorities-717.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-guzzlehttp-psr7-2.4.5/debian/patches/0005-Reject-malformed-Host-authorities-717.patch 2026-05-30 16:55:40.000000000 +0200
@@ -0,0 +1,476 @@
+From: Graham Campbell <[email protected]>
+Date: Mon, 25 May 2026 22:42:00 +0100
+Subject: Reject malformed Host authorities (#717)
+
+* Reject malformed Host authorities
+
+* Tighten Host authority validation
+
+* Mark Host authority fix as security
+
+* Remove redundant Host header type check
+
+* Share Host header authority parser
+
+* Move RFC 3986 character classes
+
+* Order RFC 3986 constants by section
+
+* Remove unused Host port flag
+
+* Avoid port zero parseRequest case
+
+Origin: backport, https://github.com/guzzle/psr7/commit/c68fe44ea6b56eb0a7ebdeb9012fb7efbc37c2d3
+Bug: https://github.com/guzzle/psr7/security/advisories/GHSA-34xg-wgjx-8xph
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2026-48998
+---
+ src/Message.php | 26 +++++++++++---
+ src/Rfc3986.php | 25 ++++++++++++++
+ src/Rfc7230.php | 83 +++++++++++++++++++++++++++++++++++++++++++++
+ src/ServerRequest.php | 31 +++++++++++------
+ src/Uri.php | 19 ++---------
+ tests/MessageTest.php | 56 ++++++++++++++++++++++++++++++
+ tests/ServerRequestTest.php | 78 ++++++++++++++++++++++++++++++++++++++++++
+ 7 files changed, 288 insertions(+), 30 deletions(-)
+ create mode 100644 src/Rfc3986.php
+
+diff --git a/src/Message.php b/src/Message.php
+index c1e15f8..ca3c1e3 100644
+--- a/src/Message.php
++++ b/src/Message.php
+@@ -174,6 +174,23 @@ final class Message
+ * @param array $headers Array of headers (each value an array).
+ */
+ public static function parseRequestUri(string $path, array $headers): string
++ {
++ $host = self::getHostFromHeaders($headers);
++
++ // If no host is found, then a full URI cannot be constructed.
++ if ($host === null) {
++ return $path;
++ }
++
++ $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
++
++ return $scheme.'://'.$host.'/'.ltrim($path, '/');
++ }
++
++ /**
++ * @param array $headers Array of headers (each value an array).
++ */
++ private static function getHostFromHeaders(array $headers): ?string
+ {
+ $hostKey = array_filter(array_keys($headers), function ($k) {
+ // Numeric array keys are converted to int by PHP.
+@@ -182,15 +199,16 @@ final class Message
+ return strtolower($k) === 'host';
+ });
+
+- // If no host is found, then a full URI cannot be constructed.
+ if (!$hostKey) {
+- return $path;
++ return null;
+ }
+
+ $host = $headers[reset($hostKey)][0];
+- $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
++ if (!is_string($host) || Rfc7230::parseHostHeader($host) === null) {
++ throw new \InvalidArgumentException('Invalid request string');
++ }
+
+- return $scheme . '://' . $host . '/' . ltrim($path, '/');
++ return $host;
+ }
+
+ /**
+diff --git a/src/Rfc3986.php b/src/Rfc3986.php
+new file mode 100644
+index 0000000..6cc2ec0
+--- /dev/null
++++ b/src/Rfc3986.php
+@@ -0,0 +1,25 @@
++<?php
++
++declare(strict_types=1);
++
++namespace GuzzleHttp\Psr7;
++
++/**
++ * @internal
++ */
++final class Rfc3986
++{
++ /**
++ * Sub-delims for use in a regex.
++ *
++ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
++ */
++ public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
++
++ /**
++ * Unreserved characters for use in a regex.
++ *
++ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
++ */
++ public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
++}
+diff --git a/src/Rfc7230.php b/src/Rfc7230.php
+index 3022401..0988953 100644
+--- a/src/Rfc7230.php
++++ b/src/Rfc7230.php
+@@ -20,4 +20,87 @@ final class Rfc7230
+ */
+ public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
+ public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
++
++ /**
++ * @return array{0: string, 1: int|null}|null
++ */
++ public static function parseHostHeader(string $authority): ?array
++ {
++ if ($authority === '') {
++ return null;
++ }
++
++ $host = $authority;
++ $port = null;
++
++ if ($authority[0] === '[') {
++ $closingBracket = strpos($authority, ']');
++ if ($closingBracket === false) {
++ return null;
++ }
++
++ $host = substr($authority, 0, $closingBracket + 1);
++ $remainder = substr($authority, $closingBracket + 1);
++ if ($remainder !== '') {
++ if ($remainder[0] !== ':') {
++ return null;
++ }
++
++ $port = self::parseAuthorityPort(substr($remainder, 1));
++ if ($port === null) {
++ return null;
++ }
++ }
++ } elseif (false !== ($colon = strpos($authority, ':'))) {
++ $host = substr($authority, 0, $colon);
++ $port = self::parseAuthorityPort(substr($authority, $colon + 1));
++ if ($port === null) {
++ return null;
++ }
++ }
++
++ if ($host === '' || !self::isValidHostHeaderHost($host)) {
++ return null;
++ }
++
++ return [$host, $port];
++ }
++
++ private static function isValidHostHeaderHost(string $host): bool
++ {
++ if (preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host)) {
++ return false;
++ }
++
++ if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
++ if ($host[0] !== '[' || substr($host, -1) !== ']') {
++ return false;
++ }
++
++ $address = substr($host, 1, -1);
++
++ return filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false
++ || preg_match('/^v[0-9a-f]+\.['.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.':]+$/iD', $address) === 1;
++ }
++
++ return strpos($host, ':') === false;
++ }
++
++ private static function parseAuthorityPort(string $port): ?int
++ {
++ if ($port === '' || !ctype_digit($port)) {
++ return null;
++ }
++
++ $normalized = ltrim($port, '0');
++ if ($normalized === '') {
++ return 0;
++ }
++
++ if (strlen($normalized) > 5 || (int) $normalized > 0xFFFF) {
++ return null;
++ }
++
++ return (int) $normalized;
++ }
+ }
+diff --git a/src/ServerRequest.php b/src/ServerRequest.php
+index 66bfa96..8815252 100644
+--- a/src/ServerRequest.php
++++ b/src/ServerRequest.php
+@@ -166,7 +166,7 @@ class ServerRequest extends Request implements ServerRequestInterface
+ public static function fromGlobals(): ServerRequestInterface
+ {
+ $method = self::getServerParam('REQUEST_METHOD') ?? 'GET';
+- $headers = self::getAllHeaders();
++ $headers = self::removeInvalidHostHeader(self::getAllHeaders());
+ $uri = self::getUriFromGlobals();
+ $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
+ $serverProtocol = self::getServerParam('SERVER_PROTOCOL');
+@@ -213,20 +213,31 @@ class ServerRequest extends Request implements ServerRequestInterface
+ }
+
+ /**
+- * @return array{0: string|null, 1: int|null}
++ * @param array<array-key, string> $headers
++ *
++ * @return array<array-key, string>
+ */
+- private static function extractHostAndPortFromAuthority(string $authority): array
++ private static function removeInvalidHostHeader(array $headers): array
+ {
+- $uri = 'http://' . $authority;
+- $parts = parse_url($uri);
+- if (!is_array($parts)) {
+- return [null, null];
++ foreach ($headers as $name => $value) {
++ if (strtolower((string) $name) !== 'host') {
++ continue;
++ }
++
++ if (Rfc7230::parseHostHeader($value) === null) {
++ unset($headers[$name]);
++ }
+ }
+
+- $host = $parts['host'] ?? null;
+- $port = $parts['port'] ?? null;
++ return $headers;
++ }
+
+- return [$host, $port];
++ /**
++ * @return array{0: string|null, 1: int|null}
++ */
++ private static function extractHostAndPortFromAuthority(string $authority): array
++ {
++ return Rfc7230::parseHostHeader($authority) ?? [null, null];
+ }
+
+ /**
+diff --git a/src/Uri.php b/src/Uri.php
+index 8292a04..5269712 100644
+--- a/src/Uri.php
++++ b/src/Uri.php
+@@ -38,19 +38,6 @@ class Uri implements UriInterface, \JsonSerializable
+ 'ldap' => 389,
+ ];
+
+- /**
+- * Unreserved characters for use in a regex.
+- *
+- * @link https://tools.ietf.org/html/rfc3986#section-2.3
+- */
+- private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
+-
+- /**
+- * Sub-delims for use in a regex.
+- *
+- * @link https://tools.ietf.org/html/rfc3986#section-2.2
+- */
+- private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
+ private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
+
+ /** @var string Uri scheme. */
+@@ -595,7 +582,7 @@ class Uri implements UriInterface, \JsonSerializable
+ }
+
+ return preg_replace_callback(
+- '/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/',
++ '/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
+ [$this, 'rawurlencodeMatchZero'],
+ $component
+ );
+@@ -692,7 +679,7 @@ class Uri implements UriInterface, \JsonSerializable
+ }
+
+ return preg_replace_callback(
+- '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
++ '/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
+ [$this, 'rawurlencodeMatchZero'],
+ $path
+ );
+@@ -712,7 +699,7 @@ class Uri implements UriInterface, \JsonSerializable
+ }
+
+ return preg_replace_callback(
+- '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
++ '/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
+ [$this, 'rawurlencodeMatchZero'],
+ $str
+ );
+diff --git a/tests/MessageTest.php b/tests/MessageTest.php
+index 203ae9e..5aa818f 100644
+--- a/tests/MessageTest.php
++++ b/tests/MessageTest.php
+@@ -107,6 +107,62 @@ class MessageTest extends TestCase
+ self::assertSame('http://foo.com/', (string)$request->getUri());
+ }
+
++ /**
++ * @dataProvider invalidHostHeaderProvider
++ */
++ public function testParseRequestRejectsInvalidHostHeader(string $host): void
++ {
++ $this->expectException(\InvalidArgumentException::class);
++
++ Psr7\Message::parseRequest("GET / HTTP/1.1\r\nHost: {$host}\r\n\r\n");
++ }
++
++ public static function invalidHostHeaderProvider(): iterable
++ {
++ yield 'userinfo delimiter' => ['[email protected]'];
++ yield 'path delimiter' => ['example.com/path'];
++ yield 'query delimiter' => ['example.com?query'];
++ yield 'fragment delimiter' => ['example.com#fragment'];
++ yield 'backslash delimiter' => ['example.com\\evil'];
++ yield 'space' => ['bad host'];
++ yield 'tab' => ["bad\thost"];
++ yield 'control character' => ['example'.chr(1).'com'];
++ yield 'delete' => ['example'.chr(0x7F).'com'];
++ yield 'multiple ports' => ['example.com:443:8443'];
++ yield 'missing closing bracket' => ['[::1'];
++ yield 'unexpected bracket suffix' => ['[::1]x'];
++ yield 'invalid ip literal' => ['[bad]'];
++ yield 'unexpected opening bracket' => ['foo[bar'];
++ yield 'unexpected closing bracket' => ['foo]bar'];
++ }
++
++ /**
++ * @dataProvider validHostHeaderProvider
++ */
++ public function testParseRequestAcceptsValidHostHeader(string $host, string $expectedUri): void
++ {
++ $request = Psr7\Message::parseRequest("GET / HTTP/1.1\r\nHost: {$host}\r\n\r\n");
++
++ self::assertSame($host, $request->getHeaderLine('Host'));
++ self::assertSame($expectedUri, (string) $request->getUri());
++ }
++
++ public static function validHostHeaderProvider(): iterable
++ {
++ yield 'host' => ['foo.com', 'http://foo.com/'];
++ yield 'https default port' => ['foo.com:443', 'https://foo.com/'];
++ yield 'non-default port' => ['foo.com:8080', 'http://foo.com:8080/'];
++ yield 'ipv6' => ['[::1]', 'http://[::1]/'];
++ yield 'ipv6 port' => ['[::1]:443', 'https://[::1]/'];
++ }
++
++ public function testParseRequestAcceptsMissingHostHeader(): void
++ {
++ $request = Psr7\Message::parseRequest("GET /abc HTTP/1.1\r\nFoo: bar\r\n\r\n");
++
++ self::assertSame('/abc', (string) $request->getUri());
++ }
++
+ public function testParsesRequestMessagesWithFullUri(): void
+ {
+ $req = "GET https://www.google.com:443/search?q=foobar HTTP/1.1\r\nHost: www.google.com\r\n\r\n";
+diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php
+index c07503e..b29a719 100644
+--- a/tests/ServerRequestTest.php
++++ b/tests/ServerRequestTest.php
+@@ -354,6 +354,46 @@ class ServerRequestTest extends TestCase
+ 'https://localhost/blog/article.php?id=10&user=foo',
+ array_merge($server, ['HTTP_HOST' => 'a:b']),
+ ],
++ 'Host header with userinfo delimiter' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => '[email protected]']),
++ ],
++ 'Host header with path delimiter' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'example.com/path']),
++ ],
++ 'Host header with query delimiter' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'example.com?x=1']),
++ ],
++ 'Host header with fragment delimiter' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'example.com#frag']),
++ ],
++ 'Host header with backslash delimiter' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'example.com\\evil']),
++ ],
++ 'Host header with space' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'bad host']),
++ ],
++ 'Host header with multiple ports' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'example.com:80:90']),
++ ],
++ 'Host header with invalid ip literal' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => '[bad]']),
++ ],
++ 'Host header with unexpected opening bracket' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'foo[bar']),
++ ],
++ 'Host header with unexpected closing bracket' => [
++ 'https://localhost/blog/article.php?id=10&user=foo',
++ array_merge($server, ['HTTP_HOST' => 'foo]bar']),
++ ],
+ 'Different port with SERVER_PORT' => [
+ 'https://www.example.org:8324/blog/article.php?id=10&user=foo',
+ array_merge($server, ['SERVER_PORT' => '8324']),
+@@ -536,6 +576,44 @@ class ServerRequestTest extends TestCase
+ self::assertSame('1.1', $server->getProtocolVersion());
+ }
+
++ /**
++ * @dataProvider invalidHostHeaderFromGlobalsProvider
++ */
++ public function testFromGlobalsDropsInvalidHostHeaderWhenUriFallsBack(string $host): void
++ {
++ if (!\function_exists('getallheaders')) {
++ self::markTestSkipped('getallheaders() is not available.');
++ }
++
++ $_SERVER = [
++ 'REQUEST_URI' => '/',
++ 'HTTP_HOST' => $host,
++ 'SERVER_PORT' => '443',
++ 'HTTPS' => 'on',
++ ];
++
++ $_COOKIE = $_POST = $_GET = $_FILES = [];
++
++ $request = ServerRequest::fromGlobals();
++
++ self::assertSame('localhost', $request->getUri()->getHost());
++ self::assertSame('localhost', $request->getHeaderLine('Host'));
++ }
++
++ public static function invalidHostHeaderFromGlobalsProvider(): iterable
++ {
++ yield 'userinfo delimiter' => ['[email protected]'];
++ yield 'path delimiter' => ['example.com/path'];
++ yield 'query delimiter' => ['example.com?x=1'];
++ yield 'fragment delimiter' => ['example.com#frag'];
++ yield 'backslash delimiter' => ['example.com\\evil'];
++ yield 'space' => ['bad host'];
++ yield 'multiple ports' => ['example.com:80:90'];
++ yield 'invalid ip literal' => ['[bad]'];
++ yield 'unexpected opening bracket' => ['foo[bar'];
++ yield 'unexpected closing bracket' => ['foo]bar'];
++ }
++
+ public function testUploadedFiles(): void
+ {
+ $request1 = new ServerRequest('GET', '/');
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/patches/0006-Reject-control-characters-in-URI-hosts-715.patch php-guzzlehttp-psr7-2.4.5/debian/patches/0006-Reject-control-characters-in-URI-hosts-715.patch
--- php-guzzlehttp-psr7-2.4.5/debian/patches/0006-Reject-control-characters-in-URI-hosts-715.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-guzzlehttp-psr7-2.4.5/debian/patches/0006-Reject-control-characters-in-URI-hosts-715.patch 2026-05-30 16:55:40.000000000 +0200
@@ -0,0 +1,186 @@
+From: Graham Campbell <[email protected]>
+Date: Mon, 25 May 2026 21:16:23 +0100
+Subject: Reject control characters in URI hosts (#715)
+
+* Reject control characters in URI hosts
+
+* Validate generated Host header values
+
+* Mark URI host fix as security
+
+Origin: backport, https://github.com/guzzle/psr7/commit/12caca7f2302477216a460fabf93a92659835a06
+Bug: https://github.com/guzzle/psr7/security/advisories/GHSA-hq7v-mx3g-29hw
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2026-49214
+---
+ src/Request.php | 4 ++++
+ src/Uri.php | 39 +++++++++++++++++++++++++++++++++++----
+ tests/RequestTest.php | 23 +++++++++++++++++++++++
+ tests/UriTest.php | 27 +++++++++++++++++++++++++++
+ 4 files changed, 89 insertions(+), 4 deletions(-)
+
+diff --git a/src/Request.php b/src/Request.php
+index b17af66..0addd9e 100644
+--- a/src/Request.php
++++ b/src/Request.php
+@@ -130,10 +130,14 @@ class Request implements RequestInterface
+ return;
+ }
+
++ Uri::assertValidHost($host);
++
+ if (($port = $this->uri->getPort()) !== null) {
+ $host .= ':' . $port;
+ }
+
++ $this->assertValue($host);
++
+ if (isset($this->headerNames['host'])) {
+ $header = $this->headerNames['host'];
+ } else {
+diff --git a/src/Uri.php b/src/Uri.php
+index 5269712..2b2cbf7 100644
+--- a/src/Uri.php
++++ b/src/Uri.php
+@@ -71,7 +71,13 @@ class Uri implements UriInterface, \JsonSerializable
+ if ($parts === false) {
+ throw new MalformedUriException("Unable to parse URI: $uri");
+ }
+- $this->applyParts($parts);
++ try {
++ $this->applyParts($parts);
++ } catch (MalformedUriException $e) {
++ throw $e;
++ } catch (\InvalidArgumentException $e) {
++ throw new MalformedUriException($e->getMessage(), 0, $e);
++ }
+ }
+ }
+ /**
+@@ -346,12 +352,34 @@ class Uri implements UriInterface, \JsonSerializable
+ public static function fromParts(array $parts): UriInterface
+ {
+ $uri = new self();
+- $uri->applyParts($parts);
+- $uri->validateState();
++ try {
++ $uri->applyParts($parts);
++ $uri->validateState();
++ } catch (MalformedUriException $e) {
++ throw $e;
++ } catch (\InvalidArgumentException $e) {
++ throw new MalformedUriException($e->getMessage(), 0, $e);
++ }
+
+ return $uri;
+ }
+
++ /**
++ * @throws \InvalidArgumentException If the host is invalid.
++ *
++ * @internal
++ */
++ public static function assertValidHost(string $host): void
++ {
++ if ($host === '') {
++ return;
++ }
++
++ if (preg_match('/[\x00-\x20\x7F]/', $host)) {
++ throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
++ }
++ }
++
+ public function getScheme(): string
+ {
+ return $this->scheme;
+@@ -599,7 +627,10 @@ class Uri implements UriInterface, \JsonSerializable
+ throw new \InvalidArgumentException('Host must be a string');
+ }
+
+- return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
++ $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
++ self::assertValidHost($host);
++
++ return $host;
+ }
+
+ /**
+diff --git a/tests/RequestTest.php b/tests/RequestTest.php
+index 6ae03e8..403d80a 100644
+--- a/tests/RequestTest.php
++++ b/tests/RequestTest.php
+@@ -9,6 +9,7 @@ use GuzzleHttp\Psr7\Request;
+ use GuzzleHttp\Psr7\Uri;
+ use PHPUnit\Framework\TestCase;
+ use Psr\Http\Message\StreamInterface;
++use Psr\Http\Message\UriInterface;
+
+ /**
+ * @covers GuzzleHttp\Psr7\MessageTrait
+@@ -305,6 +306,28 @@ class RequestTest extends TestCase
+ self::assertSame('foo.com:8125', $r->getHeaderLine('host'));
+ }
+
++ public function testGeneratedHostHeaderRejectsInvalidUriHostFromCustomUri(): void
++ {
++ $uri = $this->createMock(UriInterface::class);
++ $uri->method('getHost')->willReturn("foo\nbar");
++ $uri->method('getPort')->willReturn(null);
++
++ $this->expectException(\InvalidArgumentException::class);
++
++ new Request('GET', $uri);
++ }
++
++ public function testGeneratedHostHeaderValidatesAssembledHostWithPort(): void
++ {
++ $uri = $this->createMock(UriInterface::class);
++ $uri->method('getHost')->willReturn('example.com');
++ $uri->method('getPort')->willReturn(8080);
++
++ $request = new Request('GET', $uri);
++
++ self::assertSame('example.com:8080', $request->getHeaderLine('Host'));
++ }
++
+ /**
+ * @dataProvider provideHeaderValuesContainingNotAllowedChars
+ */
+diff --git a/tests/UriTest.php b/tests/UriTest.php
+index c807807..ec33c96 100644
+--- a/tests/UriTest.php
++++ b/tests/UriTest.php
+@@ -152,6 +152,33 @@ class UriTest extends TestCase
+ $this->expectException(\InvalidArgumentException::class);
+ (new Uri())->withHost([]);
+ }
++
++ /**
++ * @dataProvider getInvalidHostsWithControlCharacters
++ */
++ public function testHostMustRejectControlCharacters(string $host): void
++ {
++ $this->expectException(\InvalidArgumentException::class);
++
++ (new Uri())->withHost($host);
++ }
++
++ public static function getInvalidHostsWithControlCharacters(): iterable
++ {
++ for ($i = 0; $i <= 0x20; ++$i) {
++ yield 'ascii 0x'.strtoupper(dechex($i)) => ['example'.chr($i).'com'];
++ }
++
++ yield 'ascii 0x7F' => ['example'.chr(0x7F).'com'];
++ }
++
++ public function testParseUriRejectsHostWithControlCharacter(): void
++ {
++ $this->expectException(MalformedUriException::class);
++
++ new Uri("http://example.com\r\nX-Injected:%20yes/");
++ }
++
+ public function testPathMustHaveCorrectType(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
diff -Nru php-guzzlehttp-psr7-2.4.5/debian/patches/series php-guzzlehttp-psr7-2.4.5/debian/patches/series
--- php-guzzlehttp-psr7-2.4.5/debian/patches/series 2023-04-22 10:54:17.000000000 +0200
+++ php-guzzlehttp-psr7-2.4.5/debian/patches/series 2026-05-30 16:55:40.000000000 +0200
@@ -1 +1,6 @@
0001-Adapt-path-to-Debian-expectations.patch
+0002-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch
+0003-Harden-ServerRequest-globals-handling-660.patch
+0004-Normalize-global-header-values-718.patch
+0005-Reject-malformed-Host-authorities-717.patch
+0006-Reject-control-characters-in-URI-hosts-715.patch
signature.asc
Description: PGP signature
--- End Message ---