diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1a48f1b324d..a2201ec7c0a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,17 +29,17 @@ jobs: uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.3 + uses: github/codeql-action/init@v4.37.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql.yml queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v4.37.3 + uses: github/codeql-action/autobuild@v4.37.4 if: ${{ matrix.language == 'python' || matrix.language == 'javascript' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.3 + uses: github/codeql-action/analyze@v4.37.4 with: category: "/language:${{ matrix.language }}" diff --git a/CHANGES/13278.bugfix.rst b/CHANGES/13278.bugfix.rst new file mode 100644 index 00000000000..a036e50ada8 --- /dev/null +++ b/CHANGES/13278.bugfix.rst @@ -0,0 +1 @@ +Restricted cookie ``Expires`` date parsing to ASCII digits -- by :user:`dxbjavid`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 481663128fd..9e634a66290 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -216,7 +216,10 @@ class _WSConnectOptions(TypedDict, total=False): # https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2 -IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE"}) +# https://www.rfc-editor.org/info/rfc10008/#section-1-12 +IDEMPOTENT_METHODS = frozenset( + {"GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE", "QUERY"} +) _RetType_co = TypeVar( "_RetType_co", diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py index 8e945f3470d..d4e474ab98a 100644 --- a/aiohttp/cookiejar.py +++ b/aiohttp/cookiejar.py @@ -44,21 +44,23 @@ class CookieJar(AbstractCookieJar): """Implements cookie storage adhering to RFC 6265.""" + # https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1 DATE_TOKENS_RE = re.compile( r"[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]*" - r"(?P[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)" + r"(?P[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)", + re.ASCII, ) - DATE_HMS_TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})") + DATE_HMS_TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})", re.ASCII) - DATE_DAY_OF_MONTH_RE = re.compile(r"(\d{1,2})") + DATE_DAY_OF_MONTH_RE = re.compile(r"(\d{1,2})", re.ASCII) DATE_MONTH_RE = re.compile( "(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)", - re.I, + re.I | re.ASCII, ) - DATE_YEAR_RE = re.compile(r"(\d{2,4})") + DATE_YEAR_RE = re.compile(r"(\d{2,4})", re.ASCII) # calendar.timegm() fails for timestamps after datetime.datetime.max # Minus one as a loss of precision occurs when timestamp() is called. diff --git a/aiohttp/hdrs.py b/aiohttp/hdrs.py index 1082da3f4cc..1f7aa7d7d7d 100644 --- a/aiohttp/hdrs.py +++ b/aiohttp/hdrs.py @@ -16,6 +16,7 @@ METH_PATCH: Final[str] = "PATCH" METH_POST: Final[str] = "POST" METH_PUT: Final[str] = "PUT" +METH_QUERY: Final[str] = "QUERY" METH_TRACE: Final[str] = "TRACE" METH_ALL: Final[set[str]] = { diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 2017ff8b303..063d2cbe655 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -85,7 +85,7 @@ exceptiongroup==1.3.1 # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.32.0 +filelock==3.32.2 # via # python-discovery # virtualenv @@ -161,7 +161,7 @@ packaging==26.2 # wheel pathspec==1.1.1 # via mypy -pip==26.1.2 +pip==26.2 # via pip-tools pip-tools==7.6.0 # via -r requirements/dev.in @@ -333,7 +333,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.7.0 +virtualenv==21.7.1 # via pre-commit wheel==0.47.0 # via pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index 205e020c906..87ad532db85 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -83,7 +83,7 @@ exceptiongroup==1.3.1 # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.32.0 +filelock==3.32.2 # via # python-discovery # virtualenv @@ -158,7 +158,7 @@ packaging==26.2 # wheel pathspec==1.1.1 # via mypy -pip==26.1.2 +pip==26.2 # via pip-tools pip-tools==7.6.0 # via -r requirements/dev.in @@ -323,7 +323,7 @@ uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpytho # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.7.0 +virtualenv==21.7.1 # via pre-commit wheel==0.47.0 # via pip-tools diff --git a/requirements/lint.txt b/requirements/lint.txt index c7c84776f8a..0e27dcc65bd 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -42,7 +42,7 @@ exceptiongroup==1.3.1 # via # aiofastnet # pytest -filelock==3.32.0 +filelock==3.32.2 # via # python-discovery # virtualenv @@ -166,7 +166,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.7.0 +virtualenv==21.7.1 # via pre-commit yarl==1.24.5 # via aiohttp diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py index 0c24df71c0b..9fa150a5d9a 100644 --- a/tests/test_cookiejar.py +++ b/tests/test_cookiejar.py @@ -141,6 +141,12 @@ def test_date_parsing() -> None: # Invalid time assert parse_func("Tue, 1 Jan 1970 77:88:99 GMT") is None + # Invalid digits + # https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1 + assert parse_func("Tue, ١ Jan ١٩٧٠ ٠٠:٠٠:٠٠ GMT") is None + assert parse_func("Tue, 1 Jan 1970 00:00:00 GMT") is None + assert parse_func("Tue, 1 Jan 1970 ٠٠:٠٠:٠٠ GMT") is None + def test_domain_matching() -> None: test_func = CookieJar._is_domain_match diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py index 0d13b062bf0..068210bee0f 100644 --- a/tests/test_test_utils.py +++ b/tests/test_test_utils.py @@ -12,7 +12,7 @@ from yarl import URL import aiohttp -from aiohttp import web +from aiohttp import hdrs, web from aiohttp.helpers import HeadersDictProxy from aiohttp.test_utils import ( REUSE_ADDRESS, @@ -356,6 +356,29 @@ async def handler(request: web.Request) -> web.Response: assert num_requests == 2 +async def test_retry_persistent_connection_query_method( + aiohttp_client: AiohttpClient, +) -> None: + """QUERY is safe and idempotent, so it must trigger retry.""" + num_requests = 0 + + async def handler(request: web.Request) -> web.Response: + nonlocal num_requests + num_requests += 1 + if num_requests == 1: + request.protocol.force_close() + return web.Response() + + app = web.Application() + app.router.add_route(hdrs.METH_QUERY, "/", handler) + client = await aiohttp_client(app) + client.session._retry_connection = True + async with client.request(hdrs.METH_QUERY, "/") as resp: + assert resp.status == 200 + + assert num_requests == 2 + + async def test_server_context_manager(app: web.Application) -> None: async with TestServer(app) as server: async with aiohttp.ClientSession() as client: