fix(stalker): reject out-of-range calendar components in expiry dates

The multi-argument Date constructor normalizes invalid components
('2026-00-00' becomes Nov 30, 2025), fabricating an expiry and countdown
from a placeholder. Round-trip the parsed year/month/day and reject any
date that does not survive unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 committed 2026-08-02 09:16:00 +02:00
1 parent 153b17f6ad
commit 1fffa7f3fb
2 files changed
+25 -8

No files matched your search

@@ -299,6 +299,14 @@ describe('parseStalkerDate', () => {
expect(parseStalkerDate('not a date')).toBeUndefined();
});
it('rejects out-of-range calendar components instead of normalizing them', () => {
// new Date(2026, -1, 0) silently becomes Nov 30, 2025 — a
// placeholder must not fabricate an expiry.
expect(parseStalkerDate('2026-00-00')).toBeUndefined();
expect(parseStalkerDate('2026-02-30')).toBeUndefined();
expect(parseStalkerDate('2026-13-01')).toBeUndefined();
});
it('rejects negative unlimited-expiry sentinels instead of parsing them as dates', () => {
// V8 reads Date.parse('-1') as January 1, 2001 — an unlimited
// account must not render as expired.
@@ -238,14 +238,23 @@ export function parseStalkerDate(
// UTC and moves the days-left boundary. Build it in local time instead.
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
if (dateOnly) {
const [, year, month, day] = dateOnly;
const local = new Date(
Number(year),
Number(month) - 1,
Number(day)
).getTime();
return Number.isFinite(local) && local > 0
? Math.round(local / 1000)
const year = Number(dateOnly[1]);
const month = Number(dateOnly[2]);
const day = Number(dateOnly[3]);
const local = new Date(year, month - 1, day);
// Round-trip check: the multi-argument constructor normalizes
// out-of-range components ('2026-00-00' → Nov 30, 2025), which
// would fabricate an expiry from a placeholder.
if (
local.getFullYear() !== year ||
local.getMonth() !== month - 1 ||
local.getDate() !== day
) {
return undefined;
}
const timestamp = local.getTime();
return Number.isFinite(timestamp) && timestamp > 0
? Math.round(timestamp / 1000)
: undefined;
}