Your message dated Sat, 11 Jul 2026 10:42:31 +0000
with message-id <[email protected]>
and subject line Released in 12.15
has caused the Debian Bug report #1139280,
regarding bookworm-pu: package php-guzzlehttp-psr7/2.4.5-1+deb12u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1139280: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1139280
Debian Bug Tracking System
Contact [email protected] with problems
--- 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

Attachment: signature.asc
Description: PGP signature


--- End Message ---
--- Begin Message ---
Version: 12.15

This update was released as part of 12.15.

--- End Message ---

Reply via email to