From 30fb2178b47d71d491a89cf771412dea617b54e3 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 6 Aug 2026 11:30:10 +0100 Subject: [PATCH] fix(isRFC3339): reject impossible calendar dates 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. --- src/lib/isRFC3339.js | 13 ++++++++++++- test/validators.test.js | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/lib/isRFC3339.js b/src/lib/isRFC3339.js index 48b025e0f..a9a3a0828 100644 --- a/src/lib/isRFC3339.js +++ b/src/lib/isRFC3339.js @@ -21,7 +21,18 @@ const fullTime = new RegExp(`${partialTime.source}${timeOffset.source}`); const rfc3339 = new RegExp(`^${fullDate.source}[ tT]${fullTime.source}$`); +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]; +} + export default function isRFC3339(str) { assertString(str); - return rfc3339.test(str); + if (!rfc3339.test(str)) { + return false; + } + const [, year, month, day] = str.match(/^(\d{4})-(\d{2})-(\d{2})/).map(Number); + return day <= daysInMonth(year, month); } diff --git a/test/validators.test.js b/test/validators.test.js index 3d2c8e8b2..7230c27e3 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -12603,9 +12603,16 @@ describe('Validators', () => { '2010-02-18t00:23:23.33+06:00', '2010-02-18t00:23:32.33+00:00', '2010-02-18t00:23:32.33+23:00', + '2020-02-29T14:39:22Z', + '2000-02-29T00:00:00Z', ], invalid: [ '2010-02-18t00:23:32.33+24:00', + '2021-02-30T00:00:00Z', + '2021-04-31T00:00:00Z', + '2021-06-31T12:00:00Z', + '2021-02-29T00:00:00Z', + '1900-02-29T00:00:00Z', '2009-05-31 14:60:55Z', '2010-02-18t24:23.33+0600', '2009-05-00 1439,55Z',