Skip to content

fix(isRFC3339): reject impossible calendar dates - #2841

Open
spokodev wants to merge 1 commit into
validatorjs:masterfrom
spokodev:fix/rfc3339-impossible-dates
Open

fix(isRFC3339): reject impossible calendar dates#2841
spokodev wants to merge 1 commit into
validatorjs:masterfrom
spokodev:fix/rfc3339-impossible-dates

Conversation

@spokodev

@spokodev spokodev commented Aug 6, 2026

Copy link
Copy Markdown

isRFC3339 is a pure regex test, so it returns true for dates that cannot exist:

validator.isRFC3339('2021-02-30T00:00:00Z'); // true  (Feb has 28/29 days)
validator.isRFC3339('2021-04-31T00:00:00Z'); // true  (Apr has 30 days)
validator.isRFC3339('2021-06-31T12:00:00Z'); // true  (Jun has 30 days)
validator.isRFC3339('2021-02-29T00:00:00Z'); // true  (2021 is not a leap year)

RFC 3339 section 5.6 restricts date-mday to the number of days in the given month and year, which the regex cannot express ([12]\d|0[1-9]|3[01] allows 01-31 for every month). The existing test block already treats impossible dates as invalid (2009-13-19, month 00, day 00); this extends the same intent to the day-of-month maximum and the leap-year rule. For contrast, isISO8601(..., { strict: true }) already rejects 2021-02-30.

Fix

After the regex passes, compare the day against a per-month maximum, with a leap-year check for February:

function daysInMonth(year, month) {
  if (month === 2) {
    return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 29 : 28;
  }
  return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
}

The comparison is arithmetic rather than new Date(...), which avoids the legacy two-digit-year behavior of the Date parser for RFC 3339's valid years below 100. The seconds field is never inspected, so the leap-second value 14:53:60Z (already in the valid list) stays valid.

Tests

Extended the RFC 3339 block in test/validators.test.js:

  • valid: 2020-02-29 and 2000-02-29 (leap day, and the div-by-400 century case)
  • invalid: 2021-02-30, 2021-04-31, 2021-06-31, 2021-02-29, and 1900-02-29 (the div-by-100 non-leap century case)

isRFC3339 was a pure regex test, so it accepted dates that cannot exist:

  isRFC3339('2021-02-30T00:00:00Z') // true
  isRFC3339('2021-04-31T00:00:00Z') // true
  isRFC3339('2021-02-29T00:00:00Z') // true (2021 is not a leap year)

RFC 3339 section 5.6 caps date-mday at the number of days in the given
month and year, which a regex cannot express. The existing tests already
treat impossible dates as invalid (month 13, month 00, day 00); this extends
that to the day-of-month maximum and the leap-year rule.

The day is compared against a per-month maximum with a leap-year check for
February. The seconds field is never inspected, so the leap-second value
14:53:60Z stays valid.
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (d4e02ee) to head (30fb217).

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #2841   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          114       114           
  Lines         2598      2605    +7     
  Branches       658       661    +3     
=========================================
+ Hits          2598      2605    +7     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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