new file mode 100644
@@ -0,0 +1,170 @@
+From 75abab9fcf5382fb46947fe9f53142f6aa60abbc Mon Sep 17 00:00:00 2001
+From: Sam Bull <git@sambull.org>
+Date: Sun, 27 Sep 2026 18:11:00 +0100
+Subject: [PATCH] Use protocol scheme for absolute-form requests (#13821)
+ (#13827) (#13828)
+
+(cherry picked from commit a921cfd0fec28d5a9e5bf8d6f4372b8fa63c49df)
+(cherry picked from commit 93d91e37e7c6035d09e89646b3de737735ed91b5)
+(cherry picked from commit 6d4e7b01f9b951aa084a75d7356e907d4ee80411)
+
+CVE: CVE-2026-111748
+Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/6d4e7b01f9b951aa084a75d7356e907d4ee80411]
+
+Dropped tests changes during the backport
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ CHANGES/13821.bugfix.rst | 1 +
+ aiohttp/_cparser.pxd | 3 ++-
+ aiohttp/_http_parser.pyx | 29 +++++++++++++++++++++++++----
+ aiohttp/http_parser.py | 26 ++++++++++++++++++++++----
+ aiohttp/web_request.py | 7 ++++++-
+ 5 files changed, 56 insertions(+), 10 deletions(-)
+ create mode 100644 CHANGES/13821.bugfix.rst
+
+diff --git a/CHANGES/13821.bugfix.rst b/CHANGES/13821.bugfix.rst
+new file mode 100644
+index 000000000..f35418d2d
+--- /dev/null
++++ b/CHANGES/13821.bugfix.rst
+@@ -0,0 +1 @@
++Fixed the web server trusting the scheme of an absolute-form request-target -- by :user:`Dreamsorcerer`.
+diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd
+index 903f35c45..3a427002d 100644
+--- a/aiohttp/_cparser.pxd
++++ b/aiohttp/_cparser.pxd
+@@ -67,7 +67,8 @@ cdef extern from "llhttp.h":
+ HTTP_RESPONSE
+
+ enum llhttp_method:
+- HTTP_CONNECT
++ HTTP_CONNECT,
++ HTTP_OPTIONS
+
+ void llhttp_settings_init(llhttp_settings_t* settings)
+ void llhttp_init(llhttp_t* parser, llhttp_type type,
+diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
+index 0fe6084e5..4660a0c90 100644
+--- a/aiohttp/_http_parser.pyx
++++ b/aiohttp/_http_parser.pyx
+@@ -722,12 +722,20 @@ cdef class HttpRequestParser(HttpParser):
+ return
+ self._path = self._buf.decode('utf-8', 'surrogateescape')
+ try:
+- idx3 = len(self._path)
+ if self._cparser.method == cparser.HTTP_CONNECT:
+ # authority-form,
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
+- self._url = URL.build(authority=self._path, encoded=True)
+- elif idx3 > 1 and self._path[0] == '/':
++ try:
++ self._url = URL.build(authority=self._path, encoded=True)
++ host = self._url.raw_host
++ except ValueError:
++ host = None
++ if host is None:
++ raise InvalidURLError(
++ self._path.encode(errors="surrogateescape")
++ .decode("latin1")
++ )
++ elif self._path[0] == '/':
+ # origin-form,
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
+ idx1 = self._path.find("?")
+@@ -758,10 +766,23 @@ cdef class HttpRequestParser(HttpParser):
+ fragment=fragment,
+ encoded=True,
+ )
++ elif self._path == '*' and self._cparser.method == cparser.HTTP_OPTIONS:
++ # asterisk-form,
++ self._url = URL(self._path, encoded=True)
+ else:
+ # absolute-form for proxy maybe,
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
+- self._url = URL(self._path, encoded=True)
++ try:
++ self._url = URL(self._path, encoded=True)
++ host = self._url.raw_host
++ except ValueError:
++ host = None
++ # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1-4
++ if host is None:
++ raise InvalidURLError(
++ self._path.encode(errors="surrogateescape")
++ .decode("latin1")
++ )
+ finally:
+ PyByteArray_Resize(self._buf, 0)
+
+diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
+index 0c9fd086b..678aee020 100644
+--- a/aiohttp/http_parser.py
++++ b/aiohttp/http_parser.py
+@@ -685,7 +685,18 @@ class HttpRequestParser(HttpParser[RawRequestMessage]):
+ if method == "CONNECT":
+ # authority-form,
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
+- url = URL.build(authority=path, encoded=True)
++ try:
++ url = URL.build(authority=path, encoded=True)
++ host = url.raw_host
++ except ValueError:
++ # Duplicated so mypy understands that url must be defined below.
++ raise InvalidURLError(
++ path.encode(errors="surrogateescape").decode("latin1")
++ )
++ if host is None:
++ raise InvalidURLError(
++ path.encode(errors="surrogateescape").decode("latin1")
++ )
+ elif path.startswith("/"):
+ # origin-form,
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
+@@ -708,9 +719,16 @@ class HttpRequestParser(HttpParser[RawRequestMessage]):
+ else:
+ # absolute-form for proxy maybe,
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
+- url = URL(path, encoded=True)
+- if url.scheme == "":
+- # not absolute-form
++ try:
++ url = URL(path, encoded=True)
++ host = url.raw_host
++ except ValueError:
++ # Duplicated so mypy understands that url must be defined below.
++ raise InvalidURLError(
++ path.encode(errors="surrogateescape").decode("latin1")
++ )
++ # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1-4
++ if host is None:
+ raise InvalidURLError(
+ path.encode(errors="surrogateescape").decode("latin1")
+ )
+diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
+index eaed708eb..5f6c92333 100644
+--- a/aiohttp/web_request.py
++++ b/aiohttp/web_request.py
+@@ -196,8 +196,14 @@ class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin):
+ self._cache: dict[str, Any] = {}
+ url = message.url
+ if url.absolute:
++ if scheme is None and url.scheme:
++ # Absolute URL is peer-controlled, use the protocol.
++ scheme = "https" if protocol.ssl_context else "http"
+ if scheme is not None:
++ # Authority-form (CONNECT) has no scheme: leave the target
++ # unchanged unless clone(scheme=...) overrides it.
+ url = url.with_scheme(scheme)
++ self._cache["scheme"] = scheme
+ if host is not None:
+ url = url.with_host(host)
+ # absolute URL is given,
+@@ -205,7 +211,6 @@ class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin):
+ # all other properties should be good
+ self._cache["url"] = url
+ self._cache["host"] = url.host
+- self._cache["scheme"] = url.scheme
+ self._rel_url = url.relative()
+ else:
+ self._rel_url = url
@@ -9,6 +9,7 @@ SRC_URI[sha256sum] = "9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f
SRC_URI += "file://CVE-2026-111063.patch \
file://CVE-2026-111065.patch \
file://CVE-2026-111066.patch \
+ file://CVE-2026-111748.patch \
"
CVE_PRODUCT = "aiohttp:aiohttp"