diff --git a/.github/workflows/agent-scan.yml b/.github/workflows/agent-scan.yml new file mode 100644 index 00000000000..1717ebbfef5 --- /dev/null +++ b/.github/workflows/agent-scan.yml @@ -0,0 +1,18 @@ +name: AgentScan + +on: + pull_request_target: + types: + - opened + - reopened + +jobs: + agentscan: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: AgentScan + uses: MatteoGabriele/agentscan-action@v2.5.0 + with: + mode: labels diff --git a/CHANGES/13462.bugfix.rst b/CHANGES/13462.bugfix.rst new file mode 100644 index 00000000000..a4c08a20c5f --- /dev/null +++ b/CHANGES/13462.bugfix.rst @@ -0,0 +1 @@ +Fixed multipart not returning 415 when decoding failed -- by :user:`dxbjavid`. diff --git a/CHANGES/13580.bugfix.rst b/CHANGES/13580.bugfix.rst new file mode 100644 index 00000000000..62406696f14 --- /dev/null +++ b/CHANGES/13580.bugfix.rst @@ -0,0 +1 @@ +Fixed ``BaseRequest.http_range`` not accepting case-insensitive range units -- by :user:`Manny7717`. diff --git a/CHANGES/13581.bugfix.rst b/CHANGES/13581.bugfix.rst new file mode 120000 index 00000000000..402ff87b6a1 --- /dev/null +++ b/CHANGES/13581.bugfix.rst @@ -0,0 +1 @@ +13580.bugfix.rst \ No newline at end of file diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 3748922ee71..5e8f0e74486 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -258,6 +258,7 @@ Lukasz Marcin Dobrzanski Lukshya Supyal Lénárd Szolnoki Makc Belousow +Manny7717 Manuel Miranda Marat Sharafutdinov Marc Mueller diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 5f426d743c7..08c53b21e99 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -641,7 +641,8 @@ def http_range(self) -> "slice[int, int, int]": if rng is not None: try: pattern = r"^bytes=(\d*)-(\d*)$" - start, end = re.findall(pattern, rng, re.ASCII)[0] + # https://www.rfc-editor.org/info/rfc9110/#section-14.1-4 + start, end = re.findall(pattern, rng, re.ASCII | re.IGNORECASE)[0] except IndexError: # pattern was not found in header raise ValueError("range not in acceptable format") @@ -847,7 +848,11 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]": if field_ct is None or field_ct.startswith("text/"): charset = field.get_charset(default="utf-8") - out.add(field.name, value.decode(charset)) + try: + decoded = value.decode(charset) + except (LookupError, UnicodeDecodeError): + raise HTTPUnsupportedMediaType() + out.add(field.name, decoded) else: out.add(field.name, value) # type: ignore[arg-type] else: diff --git a/tests/test_web_request.py b/tests/test_web_request.py index d1b6f553a99..41e71c5e76b 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -316,6 +316,15 @@ def test_range_non_ascii() -> None: req.http_range +def test_range_to_slice_uppercase_unit() -> None: + # https://www.rfc-editor.org/info/rfc9110/#section-14.1-4 + req = make_mocked_request( + "GET", "/", headers=CIMultiDict([("RANGE", "Bytes=0-499")]) + ) + assert isinstance(req.http_range, slice) + assert req.http_range.start == 0 and req.http_range.stop == 500 + + def test_non_keepalive_on_http10() -> None: req = make_mocked_request("GET", "/", version=HttpVersion(1, 0)) assert not req.keep_alive @@ -1045,6 +1054,36 @@ async def test_multipart_formdata(protocol: BaseProtocol) -> None: assert dict(result) == {"a": "b", "c": "d"} +@pytest.mark.parametrize( + ("part_charset", "part_body"), + ( + ("not-a-real-codec", b"hello"), + ("utf-8", b"\xff\xfe"), + ), +) +async def test_multipart_formdata_field_bad_charset( + protocol: BaseProtocol, part_charset: str, part_body: bytes +) -> None: + payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + payload.feed_data( + b"-----------------------------326931944431359\r\n" + b'Content-Disposition: form-data; name="a"\r\n' + b"Content-Type: text/plain; charset=" + part_charset.encode() + b"\r\n" + b"\r\n" + part_body + b"\r\n" + b"-----------------------------326931944431359--\r\n" + ) + content_type = ( + "multipart/form-data; boundary=---------------------------326931944431359" + ) + payload.feed_eof() + req = make_mocked_request( + "POST", "/", headers={"CONTENT-TYPE": content_type}, payload=payload + ) + with pytest.raises(web.HTTPUnsupportedMediaType) as err: + await req.post() + assert err.value.status_code == 415 + + async def test_urlencoded_form_with_invalid_default_encoding( protocol: BaseProtocol, ) -> None: