diff --git a/CVE-2024-27306.patch b/CVE-2024-27306.patch
new file mode 100644
index 0000000..7acf895
--- /dev/null
+++ b/CVE-2024-27306.patch
@@ -0,0 +1,221 @@
+Index: aiohttp-3.8.5/CHANGES/8317.bugfix.rst
+===================================================================
+--- /dev/null
++++ aiohttp-3.8.5/CHANGES/8317.bugfix.rst
+@@ -0,0 +1 @@
++Escaped filenames in static view -- by :user:`bdraco`.
+Index: aiohttp-3.8.5/aiohttp/web_urldispatcher.py
+===================================================================
+--- aiohttp-3.8.5.orig/aiohttp/web_urldispatcher.py
++++ aiohttp-3.8.5/aiohttp/web_urldispatcher.py
+@@ -1,7 +1,9 @@
+ import abc
+ import asyncio
+ import base64
++import functools
+ import hashlib
++import html
+ import inspect
+ import keyword
+ import os
+@@ -87,6 +89,8 @@ PATH_SEP: Final[str] = re.escape("/")
+ _ExpectHandler = Callable[[Request], Awaitable[None]]
+ _Resolve = Tuple[Optional["UrlMappingMatchInfo"], Set[str]]
+
++html_escape = functools.partial(html.escape, quote=True)
++
+
+ class _InfoDict(TypedDict, total=False):
+ path: str
+@@ -696,7 +700,7 @@ class StaticResource(PrefixResource):
+ assert filepath.is_dir()
+
+ relative_path_to_dir = filepath.relative_to(self._directory).as_posix()
+- index_of = f"Index of /{relative_path_to_dir}"
++ index_of = f"Index of /{html_escape(relative_path_to_dir)}"
+ h1 = f"
{index_of}
"
+
+ index_list = []
+@@ -704,7 +708,7 @@ class StaticResource(PrefixResource):
+ for _file in sorted(dir_index):
+ # show file url as relative to static path
+ rel_path = _file.relative_to(self._directory).as_posix()
+- file_url = self._prefix + "/" + rel_path
++ quoted_file_url = _quote_path(f"{self._prefix}/{rel_path}")
+
+ # if file is a directory, add '/' to the end of the name
+ if _file.is_dir():
+@@ -713,9 +717,7 @@ class StaticResource(PrefixResource):
+ file_name = _file.name
+
+ index_list.append(
+- '{name}'.format(
+- url=file_url, name=file_name
+- )
++ f'{html_escape(file_name)}'
+ )
+ ul = "".format("\n".join(index_list))
+ body = f"\n{h1}\n{ul}\n"
+Index: aiohttp-3.8.5/tests/test_web_urldispatcher.py
+===================================================================
+--- aiohttp-3.8.5.orig/tests/test_web_urldispatcher.py
++++ aiohttp-3.8.5/tests/test_web_urldispatcher.py
+@@ -35,35 +35,42 @@ def tmp_dir_path(request):
+
+
+ @pytest.mark.parametrize(
+- "show_index,status,prefix,data",
++ "show_index,status,prefix,request_path,data",
+ [
+- pytest.param(False, 403, "/", None, id="index_forbidden"),
++ pytest.param(False, 403, "/", "/", None, id="index_forbidden"),
+ pytest.param(
+ True,
+ 200,
+ "/",
+- b"\n\nIndex of /.\n"
+- b"\n\nIndex of /.
\n\n\n",
+- id="index_root",
++ "/",
++ b"\n\nIndex of /.\n\n\nIndex of"
++ b' /.
\n\n\n",
+ ),
+ pytest.param(
+ True,
+ 200,
+ "/static",
+- b"\n\nIndex of /.\n"
+- b"\n\nIndex of /.
\n\n\n",
++ "/static",
++ b"\n\nIndex of /.\n\n\nIndex of"
++ b' /.
\n\n\n',
+ id="index_static",
+ ),
++ pytest.param(
++ True,
++ 200,
++ "/static",
++ "/static/my_dir",
++ b"\n\nIndex of /my_dir\n\n\n"
++ b'Index of /my_dir
\n\n\n",
++ id="index_subdir",
++ ),
+ ],
+ )
+ async def test_access_root_of_static_handler(
+- tmp_dir_path, aiohttp_client, show_index, status, prefix, data
++ tmp_dir_path, aiohttp_client, show_index, status, prefix, request_path, data
+ ) -> None:
+ # Tests the operation of static file server.
+ # Try to access the root of static file server, and make
+@@ -88,7 +95,7 @@ async def test_access_root_of_static_han
+ client = await aiohttp_client(app)
+
+ # Request the root of the static directory.
+- r = await client.get(prefix)
++ r = await client.get(request_path)
+ assert r.status == status
+
+ if data:
+@@ -97,6 +104,92 @@ async def test_access_root_of_static_han
+ assert read_ == data
+
+
++@pytest.mark.skipif(
++ not sys.platform.startswith("linux"),
++ reason="Invalid filenames on some filesystems (like Windows)",
++)
++@pytest.mark.parametrize(
++ "show_index,status,prefix,request_path,data",
++ [
++ pytest.param(False, 403, "/", "/", None, id="index_forbidden"),
++ pytest.param(
++ True,
++ 200,
++ "/",
++ "/",
++ b"\n\nIndex of /.\n\n\nIndex of"
++ b' /.
\n\n\n",
++ ),
++ pytest.param(
++ True,
++ 200,
++ "/static",
++ "/static",
++ b"\n\nIndex of /.\n\n\nIndex of"
++ b' /.
\n\n\n",
++ id="index_static",
++ ),
++ pytest.param(
++ True,
++ 200,
++ "/static",
++ "/static/
.dir",
++ b"\n\nIndex of /<img src=0 onerror=alert(1)>.dir\n\n\nIndex of /<img src=0 onerror=alert(1)>.di"
++ b'r
\n\n\n',
++ id="index_subdir",
++ ),
++ ],
++)
++async def test_access_root_of_static_handler_xss(
++ tmp_path,
++ aiohttp_client,
++ show_index,
++ status,
++ prefix,
++ request_path,
++ data,
++) -> None:
++ # Tests the operation of static file server.
++ # Try to access the root of static file server, and make
++ # sure that correct HTTP statuses are returned depending if we directory
++ # index should be shown or not.
++ # Ensure that html in file names is escaped.
++ # Ensure that links are url quoted.
++ my_file = tmp_path / "
.txt"
++ my_dir = tmp_path / "
.dir"
++ my_dir.mkdir()
++ my_file_in_dir = my_dir / "my_file_in_dir"
++
++ with my_file.open("w") as fw:
++ fw.write("hello")
++
++ with my_file_in_dir.open("w") as fw:
++ fw.write("world")
++
++ app = web.Application()
++
++ # Register global static route:
++ app.router.add_static(prefix, str(tmp_path), show_index=show_index)
++ client = await aiohttp_client(app)
++
++ # Request the root of the static directory.
++ async with await client.get(request_path) as r:
++ assert r.status == status
++
++ if data:
++ assert r.headers["Content-Type"] == "text/html; charset=utf-8"
++ read_ = await r.read()
++ assert read_ == data
++
++
+ async def test_follow_symlink(tmp_dir_path, aiohttp_client) -> None:
+ # Tests the access to a symlink, in static folder
+ data = "hello world"
diff --git a/CVE-2024-30251.patch b/CVE-2024-30251.patch
new file mode 100644
index 0000000..f87fbbb
--- /dev/null
+++ b/CVE-2024-30251.patch
@@ -0,0 +1,517 @@
+Index: aiohttp-3.8.5/aiohttp/formdata.py
+===================================================================
+--- aiohttp-3.8.5.orig/aiohttp/formdata.py
++++ aiohttp-3.8.5/aiohttp/formdata.py
+@@ -1,4 +1,5 @@
+ import io
++import warnings
+ from typing import Any, Iterable, List, Optional
+ from urllib.parse import urlencode
+
+@@ -53,7 +54,12 @@ class FormData:
+ if isinstance(value, io.IOBase):
+ self._is_multipart = True
+ elif isinstance(value, (bytes, bytearray, memoryview)):
++ msg = (
++ "In v4, passing bytes will no longer create a file field. "
++ "Please explicitly use the filename parameter or pass a BytesIO object."
++ )
+ if filename is None and content_transfer_encoding is None:
++ warnings.warn(msg, DeprecationWarning)
+ filename = name
+
+ type_options: MultiDict[str] = MultiDict({"name": name})
+@@ -81,7 +87,11 @@ class FormData:
+ "content_transfer_encoding must be an instance"
+ " of str. Got: %s" % content_transfer_encoding
+ )
+- headers[hdrs.CONTENT_TRANSFER_ENCODING] = content_transfer_encoding
++ msg = (
++ "content_transfer_encoding is deprecated. "
++ "To maintain compatibility with v4 please pass a BytesPayload."
++ )
++ warnings.warn(msg, DeprecationWarning)
+ self._is_multipart = True
+
+ self._fields.append((type_options, headers, value))
+Index: aiohttp-3.8.5/aiohttp/multipart.py
+===================================================================
+--- aiohttp-3.8.5.orig/aiohttp/multipart.py
++++ aiohttp-3.8.5/aiohttp/multipart.py
+@@ -255,13 +255,22 @@ class BodyPartReader:
+ chunk_size = 8192
+
+ def __init__(
+- self, boundary: bytes, headers: "CIMultiDictProxy[str]", content: StreamReader
++ self,
++ boundary: bytes,
++ headers: "CIMultiDictProxy[str]",
++ content: StreamReader,
++ *,
++ subtype: str = "mixed",
++ default_charset: Optional[str] = None,
+ ) -> None:
+ self.headers = headers
+ self._boundary = boundary
+ self._content = content
++ self._default_charset = default_charset
+ self._at_eof = False
+- length = self.headers.get(CONTENT_LENGTH, None)
++ self._is_form_data = subtype == "form-data"
++ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
++ length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None)
+ self._length = int(length) if length is not None else None
+ self._read_bytes = 0
+ # TODO: typeing.Deque is not supported by Python 3.5
+@@ -329,6 +338,8 @@ class BodyPartReader:
+ assert self._length is not None, "Content-Length required for chunked read"
+ chunk_size = min(size, self._length - self._read_bytes)
+ chunk = await self._content.read(chunk_size)
++ if self._content.at_eof():
++ self._at_eof = True
+ return chunk
+
+ async def _read_chunk_from_stream(self, size: int) -> bytes:
+@@ -444,7 +455,8 @@ class BodyPartReader:
+ """
+ if CONTENT_TRANSFER_ENCODING in self.headers:
+ data = self._decode_content_transfer(data)
+- if CONTENT_ENCODING in self.headers:
++ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
++ if not self._is_form_data and CONTENT_ENCODING in self.headers:
+ return self._decode_content(data)
+ return data
+
+@@ -478,7 +490,7 @@ class BodyPartReader:
+ """Returns charset parameter from Content-Type header or default."""
+ ctype = self.headers.get(CONTENT_TYPE, "")
+ mimetype = parse_mimetype(ctype)
+- return mimetype.parameters.get("charset", default)
++ return mimetype.parameters.get("charset", self._default_charset or default)
+
+ @reify
+ def name(self) -> Optional[str]:
+@@ -533,9 +545,17 @@ class MultipartReader:
+ part_reader_cls = BodyPartReader
+
+ def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None:
++ self._mimetype = parse_mimetype(headers[CONTENT_TYPE])
++ assert self._mimetype.type == "multipart", "multipart/* content type expected"
++ if "boundary" not in self._mimetype.parameters:
++ raise ValueError(
++ "boundary missed for Content-Type: %s" % headers[CONTENT_TYPE]
++ )
++
+ self.headers = headers
+ self._boundary = ("--" + self._get_boundary()).encode()
+ self._content = content
++ self._default_charset: Optional[str] = None
+ self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None
+ self._at_eof = False
+ self._at_bof = True
+@@ -587,7 +607,24 @@ class MultipartReader:
+ await self._read_boundary()
+ if self._at_eof: # we just read the last boundary, nothing to do there
+ return None
+- self._last_part = await self.fetch_next_part()
++
++ part = await self.fetch_next_part()
++ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.6
++ if (
++ self._last_part is None
++ and self._mimetype.subtype == "form-data"
++ and isinstance(part, BodyPartReader)
++ ):
++ _, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION))
++ if params.get("name") == "_charset_":
++ # Longest encoding in https://encoding.spec.whatwg.org/encodings.json
++ # is 19 characters, so 32 should be more than enough for any valid encoding.
++ charset = await part.read_chunk(32)
++ if len(charset) > 31:
++ raise RuntimeError("Invalid default charset")
++ self._default_charset = charset.strip().decode()
++ part = await self.fetch_next_part()
++ self._last_part = part
+ return self._last_part
+
+ async def release(self) -> None:
+@@ -623,19 +660,16 @@ class MultipartReader:
+ return type(self)(headers, self._content)
+ return self.multipart_reader_cls(headers, self._content)
+ else:
+- return self.part_reader_cls(self._boundary, headers, self._content)
+-
+- def _get_boundary(self) -> str:
+- mimetype = parse_mimetype(self.headers[CONTENT_TYPE])
+-
+- assert mimetype.type == "multipart", "multipart/* content type expected"
+-
+- if "boundary" not in mimetype.parameters:
+- raise ValueError(
+- "boundary missed for Content-Type: %s" % self.headers[CONTENT_TYPE]
++ return self.part_reader_cls(
++ self._boundary,
++ headers,
++ self._content,
++ subtype=self._mimetype.subtype,
++ default_charset=self._default_charset,
+ )
+
+- boundary = mimetype.parameters["boundary"]
++ def _get_boundary(self) -> str:
++ boundary = self._mimetype.parameters["boundary"]
+ if len(boundary) > 70:
+ raise ValueError("boundary %r is too long (70 chars max)" % boundary)
+
+@@ -726,6 +760,7 @@ class MultipartWriter(Payload):
+ super().__init__(None, content_type=ctype)
+
+ self._parts: List[_Part] = []
++ self._is_form_data = subtype == "form-data"
+
+ def __enter__(self) -> "MultipartWriter":
+ return self
+@@ -803,32 +838,38 @@ class MultipartWriter(Payload):
+
+ def append_payload(self, payload: Payload) -> Payload:
+ """Adds a new body part to multipart writer."""
+- # compression
+- encoding: Optional[str] = payload.headers.get(
+- CONTENT_ENCODING,
+- "",
+- ).lower()
+- if encoding and encoding not in ("deflate", "gzip", "identity"):
+- raise RuntimeError(f"unknown content encoding: {encoding}")
+- if encoding == "identity":
+- encoding = None
+-
+- # te encoding
+- te_encoding: Optional[str] = payload.headers.get(
+- CONTENT_TRANSFER_ENCODING,
+- "",
+- ).lower()
+- if te_encoding not in ("", "base64", "quoted-printable", "binary"):
+- raise RuntimeError(
+- "unknown content transfer encoding: {}" "".format(te_encoding)
++ encoding: Optional[str] = None
++ te_encoding: Optional[str] = None
++ if self._is_form_data:
++ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.7
++ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
++ assert (
++ not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING}
++ & payload.headers.keys()
+ )
+- if te_encoding == "binary":
+- te_encoding = None
+-
+- # size
+- size = payload.size
+- if size is not None and not (encoding or te_encoding):
+- payload.headers[CONTENT_LENGTH] = str(size)
++ # Set default Content-Disposition in case user doesn't create one
++ if CONTENT_DISPOSITION not in payload.headers:
++ name = f"section-{len(self._parts)}"
++ payload.set_content_disposition("form-data", name=name)
++ else:
++ # compression
++ encoding = payload.headers.get(CONTENT_ENCODING, "").lower()
++ if encoding and encoding not in ("deflate", "gzip", "identity"):
++ raise RuntimeError(f"unknown content encoding: {encoding}")
++ if encoding == "identity":
++ encoding = None
++
++ # te encoding
++ te_encoding = payload.headers.get(CONTENT_TRANSFER_ENCODING, "").lower()
++ if te_encoding not in ("", "base64", "quoted-printable", "binary"):
++ raise RuntimeError(f"unknown content transfer encoding: {te_encoding}")
++ if te_encoding == "binary":
++ te_encoding = None
++
++ # size
++ size = payload.size
++ if size is not None and not (encoding or te_encoding):
++ payload.headers[CONTENT_LENGTH] = str(size)
+
+ self._parts.append((payload, encoding, te_encoding)) # type: ignore[arg-type]
+ return payload
+@@ -886,6 +927,11 @@ class MultipartWriter(Payload):
+ async def write(self, writer: Any, close_boundary: bool = True) -> None:
+ """Write body."""
+ for part, encoding, te_encoding in self._parts:
++ if self._is_form_data:
++ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
++ assert CONTENT_DISPOSITION in part.headers
++ assert "name=" in part.headers[CONTENT_DISPOSITION]
++
+ await writer.write(b"--" + self._boundary + b"\r\n")
+ await writer.write(part._binary_headers)
+
+Index: aiohttp-3.8.5/tests/test_client_functional.py
+===================================================================
+--- aiohttp-3.8.5.orig/tests/test_client_functional.py
++++ aiohttp-3.8.5/tests/test_client_functional.py
+@@ -1158,48 +1158,6 @@ async def test_POST_DATA_with_charset_po
+ resp.close()
+
+
+-async def test_POST_DATA_with_context_transfer_encoding(aiohttp_client) -> None:
+- async def handler(request):
+- data = await request.post()
+- assert data["name"] == "text"
+- return web.Response(text=data["name"])
+-
+- app = web.Application()
+- app.router.add_post("/", handler)
+- client = await aiohttp_client(app)
+-
+- form = aiohttp.FormData()
+- form.add_field("name", "text", content_transfer_encoding="base64")
+-
+- resp = await client.post("/", data=form)
+- assert 200 == resp.status
+- content = await resp.text()
+- assert content == "text"
+- resp.close()
+-
+-
+-async def test_POST_DATA_with_content_type_context_transfer_encoding(aiohttp_client):
+- async def handler(request):
+- data = await request.post()
+- assert data["name"] == "text"
+- return web.Response(body=data["name"])
+-
+- app = web.Application()
+- app.router.add_post("/", handler)
+- client = await aiohttp_client(app)
+-
+- form = aiohttp.FormData()
+- form.add_field(
+- "name", "text", content_type="text/plain", content_transfer_encoding="base64"
+- )
+-
+- resp = await client.post("/", data=form)
+- assert 200 == resp.status
+- content = await resp.text()
+- assert content == "text"
+- resp.close()
+-
+-
+ async def test_POST_MultiDict(aiohttp_client) -> None:
+ async def handler(request):
+ data = await request.post()
+@@ -1249,7 +1207,7 @@ async def test_POST_FILES(aiohttp_client
+ client = await aiohttp_client(app)
+
+ with fname.open("rb") as f:
+- resp = await client.post("/", data={"some": f, "test": b"data"}, chunked=True)
++ resp = await client.post("/", data={"some": f, "test": io.BytesIO(b"data")}, chunked=True)
+ assert 200 == resp.status
+ resp.close()
+
+Index: aiohttp-3.8.5/tests/test_multipart.py
+===================================================================
+--- aiohttp-3.8.5.orig/tests/test_multipart.py
++++ aiohttp-3.8.5/tests/test_multipart.py
+@@ -942,6 +942,58 @@ class TestMultipartReader:
+ assert first.at_eof()
+ assert not second.at_eof()
+
++ async def test_read_form_default_encoding(self) -> None:
++ with Stream(
++ b"--:\r\n"
++ b'Content-Disposition: form-data; name="_charset_"\r\n\r\n'
++ b"ascii"
++ b"\r\n"
++ b"--:\r\n"
++ b'Content-Disposition: form-data; name="field1"\r\n\r\n'
++ b"foo"
++ b"\r\n"
++ b"--:\r\n"
++ b"Content-Type: text/plain;charset=UTF-8\r\n"
++ b'Content-Disposition: form-data; name="field2"\r\n\r\n'
++ b"foo"
++ b"\r\n"
++ b"--:\r\n"
++ b'Content-Disposition: form-data; name="field3"\r\n\r\n'
++ b"foo"
++ b"\r\n"
++ ) as stream:
++ reader = aiohttp.MultipartReader(
++ {CONTENT_TYPE: 'multipart/form-data;boundary=":"'},
++ stream,
++ )
++ field1 = await reader.next()
++ assert field1.name == "field1"
++ assert field1.get_charset("default") == "ascii"
++ field2 = await reader.next()
++ assert field2.name == "field2"
++ assert field2.get_charset("default") == "UTF-8"
++ field3 = await reader.next()
++ assert field3.name == "field3"
++ assert field3.get_charset("default") == "ascii"
++
++ async def test_read_form_invalid_default_encoding(self) -> None:
++ with Stream(
++ b"--:\r\n"
++ b'Content-Disposition: form-data; name="_charset_"\r\n\r\n'
++ b"this-value-is-too-long-to-be-a-charset"
++ b"\r\n"
++ b"--:\r\n"
++ b'Content-Disposition: form-data; name="field1"\r\n\r\n'
++ b"foo"
++ b"\r\n"
++ ) as stream:
++ reader = aiohttp.MultipartReader(
++ {CONTENT_TYPE: 'multipart/form-data;boundary=":"'},
++ stream,
++ )
++ with pytest.raises(RuntimeError, match="Invalid default charset"):
++ await reader.next()
++
+
+ async def test_writer(writer) -> None:
+ assert writer.size == 7
+@@ -1228,6 +1280,25 @@ class TestMultipartWriter:
+ part = writer._parts[0][0]
+ assert part.headers[CONTENT_TYPE] == "test/passed"
+
++ def test_set_content_disposition_after_append(self):
++ writer = aiohttp.MultipartWriter("form-data")
++ part = writer.append("some-data")
++ part.set_content_disposition("form-data", name="method")
++ assert 'name="method"' in part.headers[CONTENT_DISPOSITION]
++
++ def test_automatic_content_disposition(self):
++ writer = aiohttp.MultipartWriter("form-data")
++ writer.append_json(())
++ part = payload.StringPayload("foo")
++ part.set_content_disposition("form-data", name="second")
++ writer.append_payload(part)
++ writer.append("foo")
++
++ disps = tuple(p[0].headers[CONTENT_DISPOSITION] for p in writer._parts)
++ assert 'name="section-0"' in disps[0]
++ assert 'name="second"' in disps[1]
++ assert 'name="section-2"' in disps[2]
++
+ def test_with(self) -> None:
+ with aiohttp.MultipartWriter(boundary=":") as writer:
+ writer.append("foo")
+@@ -1278,7 +1349,6 @@ class TestMultipartWriter:
+ CONTENT_TYPE: "text/python",
+ },
+ )
+- content_length = part.size
+ await writer.write(stream)
+
+ assert part.headers[CONTENT_TYPE] == "text/python"
+@@ -1289,9 +1359,7 @@ class TestMultipartWriter:
+ assert headers == (
+ b"--:\r\n"
+ b"Content-Type: text/python\r\n"
+- b'Content-Disposition: attachments; filename="bug.py"\r\n'
+- b"Content-Length: %s"
+- b"" % (str(content_length).encode(),)
++ b'Content-Disposition: attachments; filename="bug.py"'
+ )
+
+ async def test_set_content_disposition_override(self, buf, stream):
+@@ -1305,7 +1373,6 @@ class TestMultipartWriter:
+ CONTENT_TYPE: "text/python",
+ },
+ )
+- content_length = part.size
+ await writer.write(stream)
+
+ assert part.headers[CONTENT_TYPE] == "text/python"
+@@ -1316,9 +1383,7 @@ class TestMultipartWriter:
+ assert headers == (
+ b"--:\r\n"
+ b"Content-Type: text/python\r\n"
+- b'Content-Disposition: attachments; filename="bug.py"\r\n'
+- b"Content-Length: %s"
+- b"" % (str(content_length).encode(),)
++ b'Content-Disposition: attachments; filename="bug.py"'
+ )
+
+ async def test_reset_content_disposition_header(self, buf, stream):
+@@ -1330,8 +1395,6 @@ class TestMultipartWriter:
+ headers={CONTENT_TYPE: "text/plain"},
+ )
+
+- content_length = part.size
+-
+ assert CONTENT_DISPOSITION in part.headers
+
+ part.set_content_disposition("attachments", filename="bug.py")
+@@ -1344,9 +1407,7 @@ class TestMultipartWriter:
+ b"--:\r\n"
+ b"Content-Type: text/plain\r\n"
+ b"Content-Disposition:"
+- b' attachments; filename="bug.py"\r\n'
+- b"Content-Length: %s"
+- b"" % (str(content_length).encode(),)
++ b' attachments; filename="bug.py"'
+ )
+
+
+Index: aiohttp-3.8.5/tests/test_web_functional.py
+===================================================================
+--- aiohttp-3.8.5.orig/tests/test_web_functional.py
++++ aiohttp-3.8.5/tests/test_web_functional.py
+@@ -34,7 +34,8 @@ def fname(here):
+
+ def new_dummy_form():
+ form = FormData()
+- form.add_field("name", b"123", content_transfer_encoding="base64")
++ with pytest.warns(DeprecationWarning, match="BytesPayload"):
++ form.add_field("name", b"123", content_transfer_encoding="base64")
+ return form
+
+
+@@ -429,25 +430,6 @@ async def test_release_post_data(aiohttp
+ await resp.release()
+
+
+-async def test_POST_DATA_with_content_transfer_encoding(aiohttp_client) -> None:
+- async def handler(request):
+- data = await request.post()
+- assert b"123" == data["name"]
+- return web.Response()
+-
+- app = web.Application()
+- app.router.add_post("/", handler)
+- client = await aiohttp_client(app)
+-
+- form = FormData()
+- form.add_field("name", b"123", content_transfer_encoding="base64")
+-
+- resp = await client.post("/", data=form)
+- assert 200 == resp.status
+-
+- await resp.release()
+-
+-
+ async def test_post_form_with_duplicate_keys(aiohttp_client) -> None:
+ async def handler(request):
+ data = await request.post()
+@@ -505,7 +487,8 @@ async def test_100_continue(aiohttp_clie
+ return web.Response()
+
+ form = FormData()
+- form.add_field("name", b"123", content_transfer_encoding="base64")
++ with pytest.warns(DeprecationWarning, match="BytesPayload"):
++ form.add_field("name", b"123", content_transfer_encoding="base64")
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+@@ -683,7 +666,7 @@ async def test_upload_file(aiohttp_clien
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+- resp = await client.post("/", data={"file": data})
++ resp = await client.post("/", data={"file": io.BytesIO(data)})
+ assert 200 == resp.status
+
+ await resp.release()
diff --git a/CVE-2024-52304.patch b/CVE-2024-52304.patch
new file mode 100644
index 0000000..368c7d2
--- /dev/null
+++ b/CVE-2024-52304.patch
@@ -0,0 +1,91 @@
+Index: aiohttp-3.8.5/CHANGES/9851.bugfix.rst
+===================================================================
+--- /dev/null
++++ aiohttp-3.8.5/CHANGES/9851.bugfix.rst
+@@ -0,0 +1 @@
++Fixed incorrect parsing of chunk extensions with the pure Python parser -- by :user:`bdraco`.
+Index: aiohttp-3.8.5/aiohttp/http_parser.py
+===================================================================
+--- aiohttp-3.8.5.orig/aiohttp/http_parser.py
++++ aiohttp-3.8.5/aiohttp/http_parser.py
+@@ -26,7 +26,7 @@ from yarl import URL
+
+ from . import hdrs
+ from .base_protocol import BaseProtocol
+-from .helpers import NO_EXTENSIONS, BaseTimerContext
++from .helpers import NO_EXTENSIONS, BaseTimerContext, set_exception
+ from .http_exceptions import (
+ BadHttpMessage,
+ BadStatusLine,
+@@ -770,6 +770,14 @@ class HttpPayloadParser:
+ i = chunk.find(CHUNK_EXT, 0, pos)
+ if i >= 0:
+ size_b = chunk[:i] # strip chunk-extensions
++ # Verify no LF in the chunk-extension
++ if b"\n" in chunk[i:pos]:
++ ext = repr(chunk[i:pos])
++ exc = BadHttpMessage(
++ "Unexpected LF in chunk-extension: %s" % ext
++ )
++ set_exception(self.payload, exc)
++ raise exc
+ else:
+ size_b = chunk[:pos]
+
+Index: aiohttp-3.8.5/tests/test_http_parser.py
+===================================================================
+--- aiohttp-3.8.5.orig/tests/test_http_parser.py
++++ aiohttp-3.8.5/tests/test_http_parser.py
+@@ -12,6 +12,7 @@ from yarl import URL
+
+ import aiohttp
+ from aiohttp import http_exceptions, streams
++from aiohttp.base_protocol import BaseProtocol
+ from aiohttp.http_parser import (
+ NO_EXTENSIONS,
+ DeflateBuffer,
+@@ -1202,3 +1203,27 @@ class TestDeflateBuffer:
+ dbuf.feed_eof()
+
+ assert buf.at_eof()
++
++
++async def test_parse_chunked_payload_with_lf_in_extensions_py_parser(
++ loop: asyncio.AbstractEventLoop, protocol: BaseProtocol
++) -> None:
++ """Test the py-parser with a chunked payload that has a LF in the chunk extensions."""
++ # The py parser will not raise the BadHttpMessage directly, but instead
++ # it will set the exception on the StreamReader.
++ parser = HttpRequestParserPy(
++ protocol,
++ loop,
++ max_line_size=8190,
++ max_field_size=8190,
++ )
++ payload = (
++ b"GET / HTTP/1.1\r\nHost: localhost:5001\r\n"
++ b"Transfer-Encoding: chunked\r\n\r\n2;\nxx\r\n4c\r\n0\r\n\r\n"
++ b"GET /admin HTTP/1.1\r\nHost: localhost:5001\r\n"
++ b"Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n"
++ )
++ messages, _, _ = parser.feed_data(payload)
++ reader = messages[0][1]
++ assert isinstance(reader.exception(), http_exceptions.BadHttpMessage)
++ assert "\\nxx" in str(reader.exception())
+Index: aiohttp-3.8.5/aiohttp/helpers.py
+===================================================================
+--- aiohttp-3.8.5.orig/aiohttp/helpers.py
++++ aiohttp-3.8.5/aiohttp/helpers.py
+@@ -796,8 +796,10 @@ def set_result(fut: "asyncio.Future[_T]"
+
+
+ def set_exception(fut: "asyncio.Future[_T]", exc: BaseException) -> None:
+- if not fut.done():
+- fut.set_exception(exc)
++ if asyncio.isfuture(fut) and fut.done():
++ return
++
++ fut.set_exception(exc)
+
+
+ class ChainMapProxy(Mapping[str, Any]):
diff --git a/python-aiohttp.changes b/python-aiohttp.changes
index 091b3b4..fa5d9dd 100644
--- a/python-aiohttp.changes
+++ b/python-aiohttp.changes
@@ -1,3 +1,24 @@
+-------------------------------------------------------------------
+Mon Dec 16 09:49:29 UTC 2024 - Daniel Garcia
+
+- Add upstream patch CVE-2024-27306.patch, (bsc#1223098, CVE-2024-27306)
+ * gh#aio-libs/aiohttp#8319
+
+-------------------------------------------------------------------
+Thu Dec 12 09:05:37 UTC 2024 - Daniel Garcia
+
+- Add upstream patch CVE-2024-30251.patch (bsc#1223726, CVE-2024-30251)
+ Include three upstream commits:
+ * gh#aio-libs/aiohttp@cebe526b9c34#diff-5954cadbd6b57b1921fc64d0e6a8f81717127873d9ccec33184d2f971fe6834f
+ * gh#aio-libs/aiohttp@7eecdff163cc#diff-d582bf292efb8e19696d88c895b99e0937687cb909d9d00b5c2f1d948a5cbae5
+ * gh#aio-libs/aiohttp@f21c6f2ca512#diff-d582bf292efb8e19696d88c895b99e0937687cb909d9d00b5c2f1d948a5cbae5
+
+-------------------------------------------------------------------
+Tue Nov 19 09:54:47 UTC 2024 - Daniel Garcia
+
+- Add upstream patch CVE-2024-52304.patch, gh#aio-libs/aiohttp@259edc369075
+ (bsc#1233447, CVE-2024-52304)
+
-------------------------------------------------------------------
Mon Sep 11 20:43:01 UTC 2023 - Dirk Müller
diff --git a/python-aiohttp.spec b/python-aiohttp.spec
index 5b8828e..1530a0a 100644
--- a/python-aiohttp.spec
+++ b/python-aiohttp.spec
@@ -29,6 +29,15 @@ URL: https://github.com/aio-libs/aiohttp
Source: https://files.pythonhosted.org/packages/source/a/aiohttp/aiohttp-%{version}.tar.gz
# PATCH-FIX-UPSTREAM Update-update_query-calls-to-work-with-latest-yarl.patch gh#aio-libs/aiohttp#7260
Patch1: Update-update_query-calls-to-work-with-latest-yarl.patch
+# PATCH-FIX-UPSTREAM CVE-2024-52304.patch bsc#1233447, gh#aio-libs/aiohttp@259edc369075
+Patch2: CVE-2024-52304.patch
+# PATCH-FIX-UPSTREAM CVE-2024-30251.patch bsc#1223726
+# gh#aio-libs/aiohttp@cebe526b9c34#diff-5954cadbd6b57b1921fc64d0e6a8f81717127873d9ccec33184d2f971fe6834f
+# gh#aio-libs/aiohttp@7eecdff163cc#diff-d582bf292efb8e19696d88c895b99e0937687cb909d9d00b5c2f1d948a5cbae5
+# gh#aio-libs/aiohttp@f21c6f2ca512#diff-d582bf292efb8e19696d88c895b99e0937687cb909d9d00b5c2f1d948a5cbae5
+Patch3: CVE-2024-30251.patch
+# PATCH-FIX-UPSTREAM CVE-2024-27306.patch bsc#1223098, gh#aio-libs/aiohttp#8319
+Patch4: CVE-2024-27306.patch
Requires: python-aiosignal >= 1.1.2
Requires: python-attrs >= 17.3.0
Requires: python-frozenlist >= 1.1.1