Skip to content

cookiejar: reject impossible calendar dates that calendar.timegm silently normalises#13124

Open
HrachShah wants to merge 2 commits into
aio-libs:masterfrom
HrachShah:fix/parse-date-reject-impossible-calendar
Open

cookiejar: reject impossible calendar dates that calendar.timegm silently normalises#13124
HrachShah wants to merge 2 commits into
aio-libs:masterfrom
HrachShah:fix/parse-date-reject-impossible-calendar

Conversation

@HrachShah

Copy link
Copy Markdown

CookieJar._parse_date fed the assembled year/month/day/hour/minute/second tuple straight to calendar.timegm, which silently normalises invalid combinations: Feb 30 -> Mar 2, Apr 31 -> May 1, Feb 29 in a non-leap year -> Mar 1, hour=24 -> next day at 00:00, second=60 -> next minute at :00. A server that sends Set-Cookie: ...; expires=Thu, 30 Feb 2023 12:00:00 GMT (typo for Mar 2) was being accepted and pinned to 2023-03-02 12:00:00 — the cookie would expire one day later than the server's intent.

The pre-fix code only checked 1 <= day <= 31, 0 <= month <= 12 is implied, and the time-component upper bounds. None of those catch Feb 30/31, Apr/Jun/Sep/Nov 31, or Feb 29 in a non-leap year. The fix uses calendar.monthrange to learn the actual last day for the parsed year+month and rejects when the parsed day exceeds it; a non-leap Feb 29, the original buggy case, now returns None and the cookie is dropped. hour=24, minute=60, second=60 are rejected the same way as their existing out-of-range siblings.

Tests cover Feb 30/31 (2023), Apr/Jun/Sep/Nov 31 (2023), Feb 29 2023, day 0/32, second=60, hour=24, plus the existing valid Feb 29 2024 leap-year case to confirm the normal happy path is unchanged. Full aiohttp test_cookiejar.py (91 tests) and test_helpers.py (231 tests) pass.

Zo Bot added 2 commits July 12, 2026 01:47
…ntly normalises

A Set-Cookie / cookie expiration date like `Thu, 30 Feb 2023 12:00:00 GMT`,
`Mon, 31 Apr 2023 12:00:00 GMT`, or `Wed, 29 Feb 2023 12:00:00 GMT` (Feb 29
in a non-leap year) is not a real date, but CookieJar._parse_date used to
pass it straight to calendar.timegm, which silently normalises the values:
Feb 30 becomes Mar 2, Apr 31 becomes May 1, and Feb 29 2023 becomes Mar 1.
The cookie then expired a couple of days later than the server told it to,
which the user had no way to detect because the validation surface was
zero — the function returned a plausible-looking timestamp.

The same silent normalisation applies to hour=24 (which timegm treats as
the next day's midnight) and second=60 (treated as second=0 of the next
minute). The existing check covered 1 <= day <= 31 but not the day-vs-month
relationship, not month 0/13, and not hour=24 or second=60.

After validating day/month/year ranges, look up the last day of the given
month with calendar.monthrange and reject day > last_day. Keep the existing
1 <= day <= 31 / 0 < month < 13 range checks up front and the time range
checks; reject hour > 23 and second > 59 explicitly so neither path slips
through to timegm. Month 0/13, day 0/32, Feb 30/31, Apr/Jun/Sep/Nov 31,
Feb 29 in a non-leap year, hour 24, minute 60, and second 60 all now return
None. Feb 29 2024 (a real leap-year date) still parses correctly.
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.96%. Comparing base (64313cf) to head (4fda189).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
aiohttp/cookiejar.py 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #13124      +/-   ##
==========================================
- Coverage   98.97%   98.96%   -0.01%     
==========================================
  Files         131      131              
  Lines       48641    48659      +18     
  Branches     2525     2526       +1     
==========================================
+ Hits        48141    48157      +16     
- Misses        376      378       +2     
  Partials      124      124              
Flag Coverage Δ
Autobahn 22.12% <0.00%> (-0.01%) ⬇️
CI-GHA 98.91% <90.00%> (-0.01%) ⬇️
OS-Linux 98.67% <90.00%> (-0.02%) ⬇️
OS-Windows 97.07% <90.00%> (-0.01%) ⬇️
OS-macOS 97.97% <90.00%> (-0.01%) ⬇️
Py-3.10 98.15% <90.00%> (-0.01%) ⬇️
Py-3.11 98.43% <90.00%> (-0.01%) ⬇️
Py-3.12 98.51% <90.00%> (-0.01%) ⬇️
Py-3.13 98.48% <90.00%> (-0.02%) ⬇️
Py-3.14 98.51% <90.00%> (-0.01%) ⬇️
Py-3.14t 97.61% <90.00%> (+<0.01%) ⬆️
Py-pypy-3.11 97.47% <90.00%> (-0.02%) ⬇️
VM-macos 97.97% <90.00%> (-0.01%) ⬇️
VM-ubuntu 98.67% <90.00%> (-0.02%) ⬇️
VM-windows 97.07% <90.00%> (-0.01%) ⬇️
cython-coverage 37.92% <10.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@codspeed-hq

codspeed-hq Bot commented Jul 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 83 untouched benchmarks
⏩ 83 skipped benchmarks1


Comparing HrachShah:fix/parse-date-reject-impossible-calendar (4fda189) with master (64313cf)

Open in CodSpeed

Footnotes

  1. 83 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant