Version in base suite: 2.7.1-1 Base version: php-guzzlehttp-psr7_2.7.1-1 Target version: php-guzzlehttp-psr7_2.7.1-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/php-guzzlehttp-psr7/php-guzzlehttp-psr7_2.7.1-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/php-guzzlehttp-psr7/php-guzzlehttp-psr7_2.7.1-1+deb13u1.dsc changelog | 13 control | 2 gbp.conf | 2 patches/0004-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch | 61 + patches/0004-Modernize-PHPUnit-syntax.patch | 87 + patches/0005-Harden-ServerRequest-globals-handling-660.patch | 181 +++ patches/0006-Normalize-global-header-values-718.patch | 102 ++ patches/0007-Reject-malformed-Host-authorities-717.patch | 476 ++++++++++ patches/0008-Reject-control-characters-in-URI-hosts-715.patch | 185 +++ patches/series | 5 10 files changed, 1097 insertions(+), 17 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpuwk4gqci/php-guzzlehttp-psr7_2.7.1-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpuwk4gqci/php-guzzlehttp-psr7_2.7.1-1+deb13u1.dsc: no acceptable signature found diff -Nru php-guzzlehttp-psr7-2.7.1/debian/changelog php-guzzlehttp-psr7-2.7.1/debian/changelog --- php-guzzlehttp-psr7-2.7.1/debian/changelog 2025-03-31 10:19:42.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/changelog 2026-05-30 11:39:01.000000000 +0000 @@ -1,3 +1,16 @@ +php-guzzlehttp-psr7 (2.7.1-1+deb13u1) trixie; 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) + * Track debian/trixie branch + + -- David Prévot Sat, 30 May 2026 13:39:01 +0200 + php-guzzlehttp-psr7 (2.7.1-1) unstable; urgency=medium [ Graham Campbell ] diff -Nru php-guzzlehttp-psr7-2.7.1/debian/control php-guzzlehttp-psr7-2.7.1/debian/control --- php-guzzlehttp-psr7-2.7.1/debian/control 2025-03-31 10:19:42.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/control 2026-05-30 11:39:01.000000000 +0000 @@ -15,7 +15,7 @@ psmisc Standards-Version: 4.7.2 Homepage: https://github.com/guzzle/psr7 -Vcs-Git: https://salsa.debian.org/php-team/pear/php-guzzlehttp-psr7.git +Vcs-Git: https://salsa.debian.org/php-team/pear/php-guzzlehttp-psr7.git -b debian/trixie Vcs-Browser: https://salsa.debian.org/php-team/pear/php-guzzlehttp-psr7 Rules-Requires-Root: no diff -Nru php-guzzlehttp-psr7-2.7.1/debian/gbp.conf php-guzzlehttp-psr7-2.7.1/debian/gbp.conf --- php-guzzlehttp-psr7-2.7.1/debian/gbp.conf 2024-07-21 00:49:36.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/gbp.conf 2026-05-30 11:39:01.000000000 +0000 @@ -1,5 +1,5 @@ [DEFAULT] -debian-branch = debian/latest +debian-branch = debian/trixie filter = [ '.gitattributes' ] pristine-tar = True upstream-vcs-tag = %(version%~%-)s diff -Nru php-guzzlehttp-psr7-2.7.1/debian/patches/0004-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch php-guzzlehttp-psr7-2.7.1/debian/patches/0004-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch --- php-guzzlehttp-psr7-2.7.1/debian/patches/0004-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/0004-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch 2026-05-30 11:39:01.000000000 +0000 @@ -0,0 +1,61 @@ +From: =?utf-8?q?Ey=C3=BCp_Can_Akman?= +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 a7cdfb0..bef82e2 100644 +--- a/src/Uri.php ++++ b/src/Uri.php +@@ -51,7 +51,7 @@ class Uri implements UriInterface, \JsonSerializable + * @see https://datatracker.ietf.org/doc/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 = ''; +@@ -661,7 +661,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 185b7f1..3fb0716 100644 +--- a/tests/UriTest.php ++++ b/tests/UriTest.php +@@ -390,6 +390,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.7.1/debian/patches/0004-Modernize-PHPUnit-syntax.patch php-guzzlehttp-psr7-2.7.1/debian/patches/0004-Modernize-PHPUnit-syntax.patch --- php-guzzlehttp-psr7-2.7.1/debian/patches/0004-Modernize-PHPUnit-syntax.patch 2025-03-31 10:18:24.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/0004-Modernize-PHPUnit-syntax.patch 2026-05-30 11:39:01.000000000 +0000 @@ -6,20 +6,21 @@ tests/AppendStreamTest.php | 5 ++--- tests/FnStreamTest.php | 5 ++--- tests/HeaderTest.php | 13 ++++--------- + tests/MessageTest.php | 9 +++------ tests/PumpStreamTest.php | 5 ++--- tests/QueryTest.php | 9 +++------ tests/RequestTest.php | 21 ++++++--------------- tests/ResponseTest.php | 21 +++++++-------------- - tests/ServerRequestTest.php | 9 +++------ + tests/ServerRequestTest.php | 13 ++++--------- tests/StreamDecoratorTraitTest.php | 5 ++--- tests/StreamTest.php | 12 ++++-------- tests/UploadedFileTest.php | 21 ++++++--------------- tests/UriComparatorTest.php | 5 ++--- tests/UriNormalizerTest.php | 13 ++++--------- tests/UriResolverTest.php | 13 ++++--------- - tests/UriTest.php | 21 ++++++--------------- + tests/UriTest.php | 25 +++++++------------------ tests/UtilsTest.php | 9 ++++----- - 16 files changed, 61 insertions(+), 126 deletions(-) + 17 files changed, 66 insertions(+), 138 deletions(-) diff --git a/tests/AppendStreamTest.php b/tests/AppendStreamTest.php index 302470a..b0c6a2e 100644 @@ -111,6 +112,40 @@ public function testSplitList($header, $result): void { self::assertSame($result, Psr7\Header::splitList($header)); +diff --git a/tests/MessageTest.php b/tests/MessageTest.php +index 42909be..4df397a 100644 +--- a/tests/MessageTest.php ++++ b/tests/MessageTest.php +@@ -6,6 +6,7 @@ namespace GuzzleHttp\Tests\Psr7; + + use GuzzleHttp\Psr7; + use GuzzleHttp\Psr7\FnStream; ++use PHPUnit\Framework\Attributes\DataProvider; + use PHPUnit\Framework\TestCase; + + class MessageTest extends TestCase +@@ -107,9 +108,7 @@ class MessageTest extends TestCase + self::assertSame('http://foo.com/', (string) $request->getUri()); + } + +- /** +- * @dataProvider invalidHostHeaderProvider +- */ ++ #[DataProvider('invalidHostHeaderProvider')] + public function testParseRequestRejectsInvalidHostHeader(string $host): void + { + $this->expectException(\InvalidArgumentException::class); +@@ -136,9 +135,7 @@ class MessageTest extends TestCase + yield 'unexpected closing bracket' => ['foo]bar']; + } + +- /** +- * @dataProvider validHostHeaderProvider +- */ ++ #[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"); diff --git a/tests/PumpStreamTest.php b/tests/PumpStreamTest.php index a4f3f2c..52ded59 100644 --- a/tests/PumpStreamTest.php @@ -169,7 +204,7 @@ { $result = Psr7\Query::parse($input, false); diff --git a/tests/RequestTest.php b/tests/RequestTest.php -index 19d71c3..92c9733 100644 +index 3a62c57..0ea5069 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -7,6 +7,7 @@ namespace GuzzleHttp\Tests\Psr7; @@ -179,8 +214,8 @@ +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; - -@@ -95,18 +96,14 @@ class RequestTest extends TestCase + use Psr\Http\Message\UriInterface; +@@ -96,18 +97,14 @@ class RequestTest extends TestCase self::assertSame($u1, $r1->getUri()); } @@ -201,7 +236,7 @@ public function testWithInvalidMethods($method): void { $r = new Request('get', '/'); -@@ -197,9 +194,7 @@ class RequestTest extends TestCase +@@ -198,9 +195,7 @@ class RequestTest extends TestCase self::assertSame('', $r->getHeaderLine('Bar')); } @@ -212,7 +247,7 @@ public function testContainsNotAllowedCharsOnHeaderField($header): void { $this->expectExceptionMessage( -@@ -222,9 +217,7 @@ class RequestTest extends TestCase +@@ -223,9 +218,7 @@ class RequestTest extends TestCase return [[' key '], ['key '], [' key'], ['key/'], ['key('], ['key\\'], [' ']]; } @@ -223,8 +258,8 @@ public function testContainsAllowedCharsOnHeaderField($header): void { $r = new Request( -@@ -306,9 +299,7 @@ class RequestTest extends TestCase - self::assertSame('foo.com:8125', $r->getHeaderLine('host')); +@@ -329,9 +322,7 @@ class RequestTest extends TestCase + self::assertSame('example.com:8080', $request->getHeaderLine('Host')); } - /** @@ -317,7 +352,7 @@ { $response = new Response(); diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php -index f770078..50d173b 100644 +index 914f949..b81b663 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -7,6 +7,7 @@ namespace GuzzleHttp\Tests\Psr7; @@ -339,7 +374,7 @@ public function testNormalizeFiles($files, $expected): void { $result = ServerRequest::normalizeFiles($files); -@@ -369,9 +368,7 @@ class ServerRequestTest extends TestCase +@@ -433,9 +432,7 @@ class ServerRequestTest extends TestCase ]; } @@ -350,6 +385,17 @@ public function testGetUriFromGlobals($expected, $serverParams): void { $_SERVER = $serverParams; +@@ -576,9 +573,7 @@ class ServerRequestTest extends TestCase + self::assertSame('1.1', $server->getProtocolVersion()); + } + +- /** +- * @dataProvider invalidHostHeaderFromGlobalsProvider +- */ ++ #[DataProvider('invalidHostHeaderFromGlobalsProvider')] + public function testFromGlobalsDropsInvalidHostHeaderWhenUriFallsBack(string $host): void + { + if (!\function_exists('getallheaders')) { diff --git a/tests/StreamDecoratorTraitTest.php b/tests/StreamDecoratorTraitTest.php index aaa9f28..11e699c 100644 --- a/tests/StreamDecoratorTraitTest.php @@ -598,7 +644,7 @@ { $baseUri = new Uri($base); diff --git a/tests/UriTest.php b/tests/UriTest.php -index 185b7f1..b0b39a4 100644 +index da229a7..01d7df5 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -6,6 +6,7 @@ namespace GuzzleHttp\Tests\Psr7; @@ -642,7 +688,18 @@ public function testInvalidUrisThrowException(string $invalidUri): void { $this->expectException(MalformedUriException::class); -@@ -205,9 +200,7 @@ class UriTest extends TestCase +@@ -153,9 +148,7 @@ class UriTest extends TestCase + (new Uri())->withHost([]); + } + +- /** +- * @dataProvider getInvalidHostsWithControlCharacters +- */ ++ #[DataProvider('getInvalidHostsWithControlCharacters')] + public function testHostMustRejectControlCharacters(string $host): void + { + $this->expectException(\InvalidArgumentException::class); +@@ -231,9 +224,7 @@ class UriTest extends TestCase self::assertSame('0://0:0@0/0?0#0', (string) $uri); } @@ -653,7 +710,7 @@ public function testIsDefaultPort(string $scheme, ?int $port, bool $isDefaultPort): void { $uri = $this->createMock(UriInterface::class); -@@ -553,9 +546,7 @@ class UriTest extends TestCase +@@ -590,9 +581,7 @@ class UriTest extends TestCase ]; } diff -Nru php-guzzlehttp-psr7-2.7.1/debian/patches/0005-Harden-ServerRequest-globals-handling-660.patch php-guzzlehttp-psr7-2.7.1/debian/patches/0005-Harden-ServerRequest-globals-handling-660.patch --- php-guzzlehttp-psr7-2.7.1/debian/patches/0005-Harden-ServerRequest-globals-handling-660.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/0005-Harden-ServerRequest-globals-handling-660.patch 2026-05-30 11:39:01.000000000 +0000 @@ -0,0 +1,181 @@ +From: Graham Campbell +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 3cc9534..bc795fc 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 f770078..dbd2288 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.7.1/debian/patches/0006-Normalize-global-header-values-718.patch php-guzzlehttp-psr7-2.7.1/debian/patches/0006-Normalize-global-header-values-718.patch --- php-guzzlehttp-psr7-2.7.1/debian/patches/0006-Normalize-global-header-values-718.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/0006-Normalize-global-header-values-718.patch 2026-05-30 11:39:01.000000000 +0000 @@ -0,0 +1,102 @@ +From: Graham Campbell +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 bc795fc..341af9c 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 ++ */ ++ private static function getAllHeaders(): array ++ { ++ return self::normalizeHeaderValues(getallheaders()); ++ } ++ ++ /** ++ * @param array $headers ++ * ++ * @return array ++ */ ++ 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 dbd2288..939ab19 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.7.1/debian/patches/0007-Reject-malformed-Host-authorities-717.patch php-guzzlehttp-psr7-2.7.1/debian/patches/0007-Reject-malformed-Host-authorities-717.patch --- php-guzzlehttp-psr7-2.7.1/debian/patches/0007-Reject-malformed-Host-authorities-717.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/0007-Reject-malformed-Host-authorities-717.patch 2026-05-30 11:39:01.000000000 +0000 @@ -0,0 +1,476 @@ +From: Graham Campbell +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 5561a51..d3aafca 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 @@ ++@,;:\\\"/[\]?={}\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 341af9c..3f87f96 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 $headers ++ * ++ * @return array + */ +- 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 bef82e2..54bbb0a 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. +- * +- * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 +- */ +- private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; +- +- /** +- * Sub-delims for use in a regex. +- * +- * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 +- */ +- private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B']; + + /** @var string Uri scheme. */ +@@ -596,7 +583,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 + ); +@@ -695,7 +682,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 + ); +@@ -715,7 +702,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 1a43152..42909be 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' => ['trusted.example@evil.example']; ++ 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 939ab19..914f949 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' => 'trusted.example@evil.example']), ++ ], ++ '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' => ['trusted.example@evil.example']; ++ 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.7.1/debian/patches/0008-Reject-control-characters-in-URI-hosts-715.patch php-guzzlehttp-psr7-2.7.1/debian/patches/0008-Reject-control-characters-in-URI-hosts-715.patch --- php-guzzlehttp-psr7-2.7.1/debian/patches/0008-Reject-control-characters-in-URI-hosts-715.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/0008-Reject-control-characters-in-URI-hosts-715.patch 2026-05-30 11:39:01.000000000 +0000 @@ -0,0 +1,185 @@ +From: Graham Campbell +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 | 26 ++++++++++++++++++++++++++ + 4 files changed, 88 insertions(+), 4 deletions(-) + +diff --git a/src/Request.php b/src/Request.php +index faafe1a..743c5d2 100644 +--- a/src/Request.php ++++ b/src/Request.php +@@ -132,10 +132,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 54bbb0a..b26e12d 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); ++ } + } + } + +@@ -347,12 +353,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; +@@ -600,7 +628,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 19d71c3..3a62c57 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 +@@ -306,6 +307,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 3fb0716..da229a7 100644 +--- a/tests/UriTest.php ++++ b/tests/UriTest.php +@@ -153,6 +153,32 @@ class UriTest extends TestCase + (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.7.1/debian/patches/series php-guzzlehttp-psr7-2.7.1/debian/patches/series --- php-guzzlehttp-psr7-2.7.1/debian/patches/series 2025-03-31 10:18:24.000000000 +0000 +++ php-guzzlehttp-psr7-2.7.1/debian/patches/series 2026-05-30 11:39:01.000000000 +0000 @@ -1,4 +1,9 @@ 0001-Adapt-path-to-Debian-expectations.patch 0002-Compatibility-with-recent-PHPUnit-10.patch 0003-Make-provider-functions-static-PHPUnit-11-Fix.patch +0004-2.8-Encode-plus-sign-in-withQueryValue-and-withQuery.patch +0005-Harden-ServerRequest-globals-handling-660.patch +0006-Normalize-global-header-values-718.patch +0007-Reject-malformed-Host-authorities-717.patch +0008-Reject-control-characters-in-URI-hosts-715.patch 0004-Modernize-PHPUnit-syntax.patch