From 385febc14d49f309ae65001693f7691d03028ed2 Mon Sep 17 00:00:00 2001 From: Renfei Wang Date: Mon, 27 Jul 2026 08:28:06 -0400 Subject: [PATCH] fix(packages): reject impossible manifest timestamps --- macos/assets/theme-package-validator.mjs | 23 ++++++++++++++++++-- macos/tests/theme-package-validator.test.mjs | 18 +++++++++++++++ runtime/theme-package-validator.mjs | 23 ++++++++++++++++++-- windows/assets/theme-package-validator.mjs | 23 ++++++++++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/macos/assets/theme-package-validator.mjs b/macos/assets/theme-package-validator.mjs index 275fcfd1..654eeafe 100644 --- a/macos/assets/theme-package-validator.mjs +++ b/macos/assets/theme-package-validator.mjs @@ -79,7 +79,7 @@ const KEY_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/u; const PROVENANCE_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u; const COLOR_PATTERN = /^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|#[0-9a-fA-F]{3,4}|rgb\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*\)|rgba\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*(0|1|1\.0|0?\.[0-9]{1,6})\s*\))$/; -const RFC3339_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?(?:Z|[+-][0-9]{2}:[0-9]{2})$/; +const RFC3339_PATTERN = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]{1,9}))?(?:Z|([+-])([0-9]{2}):([0-9]{2}))$/; const OPEN_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0); const decoder = new TextDecoder("utf-8", { fatal: true }); const scriptPath = fileURLToPath(import.meta.url); @@ -265,7 +265,26 @@ async function sourceFileNames(root) { function validateTimestamp(value) { assertString(value, "manifest.createdAt", { min: 1, max: 40, pattern: RFC3339_PATTERN, controls: null }); - if (!Number.isFinite(Date.parse(value))) fail("manifest.createdAt is not a valid date-time"); + const match = RFC3339_PATTERN.exec(value); + if (!match) fail("manifest.createdAt is not a valid date-time"); + + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + const hour = Number(match[4]); + const minute = Number(match[5]); + const second = Number(match[6]); + const offsetHour = match[9] === undefined ? 0 : Number(match[9]); + const offsetMinute = match[10] === undefined ? 0 : Number(match[10]); + const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const calendarValid = month >= 1 && month <= 12 + && day >= 1 && day <= (daysInMonth[month - 1] ?? 0) + && hour <= 23 && minute <= 59 && second <= 59 + && offsetHour <= 23 && offsetMinute <= 59; + if (!calendarValid || !Number.isFinite(Date.parse(value))) { + fail("manifest.createdAt is not a valid date-time"); + } } function validateOfficialTheme(value) { diff --git a/macos/tests/theme-package-validator.test.mjs b/macos/tests/theme-package-validator.test.mjs index b5531f97..d39d4c2f 100644 --- a/macos/tests/theme-package-validator.test.mjs +++ b/macos/tests/theme-package-validator.test.mjs @@ -172,6 +172,24 @@ try { "theme.json", ]); + const impossibleTimestamp = await makeOfficial("official-impossible-timestamp", { + mutateManifest: (manifest) => { manifest.createdAt = "2026-02-30T00:00:00Z"; }, + }); + await expectRejected( + impossibleTimestamp.source, + "macos", + /createdAt is not a valid date-time/, + "impossible-timestamp", + ); + + const leapTimestamp = await makeOfficial("official-valid-leap-timestamp", { + mutateManifest: (manifest) => { + manifest.createdAt = "2024-02-29T23:59:59.123456789+05:30"; + }, + }); + const leapResult = await validate(leapTimestamp.source, "windows", "valid-leap-timestamp"); + assert.equal(leapResult.output.format, "official"); + const legacyOfficial = await makeOfficial("legacy-official-no-css", { css: false }); await expectRejected( legacyOfficial.source, diff --git a/runtime/theme-package-validator.mjs b/runtime/theme-package-validator.mjs index 275fcfd1..654eeafe 100644 --- a/runtime/theme-package-validator.mjs +++ b/runtime/theme-package-validator.mjs @@ -79,7 +79,7 @@ const KEY_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/u; const PROVENANCE_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u; const COLOR_PATTERN = /^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|#[0-9a-fA-F]{3,4}|rgb\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*\)|rgba\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*(0|1|1\.0|0?\.[0-9]{1,6})\s*\))$/; -const RFC3339_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?(?:Z|[+-][0-9]{2}:[0-9]{2})$/; +const RFC3339_PATTERN = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]{1,9}))?(?:Z|([+-])([0-9]{2}):([0-9]{2}))$/; const OPEN_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0); const decoder = new TextDecoder("utf-8", { fatal: true }); const scriptPath = fileURLToPath(import.meta.url); @@ -265,7 +265,26 @@ async function sourceFileNames(root) { function validateTimestamp(value) { assertString(value, "manifest.createdAt", { min: 1, max: 40, pattern: RFC3339_PATTERN, controls: null }); - if (!Number.isFinite(Date.parse(value))) fail("manifest.createdAt is not a valid date-time"); + const match = RFC3339_PATTERN.exec(value); + if (!match) fail("manifest.createdAt is not a valid date-time"); + + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + const hour = Number(match[4]); + const minute = Number(match[5]); + const second = Number(match[6]); + const offsetHour = match[9] === undefined ? 0 : Number(match[9]); + const offsetMinute = match[10] === undefined ? 0 : Number(match[10]); + const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const calendarValid = month >= 1 && month <= 12 + && day >= 1 && day <= (daysInMonth[month - 1] ?? 0) + && hour <= 23 && minute <= 59 && second <= 59 + && offsetHour <= 23 && offsetMinute <= 59; + if (!calendarValid || !Number.isFinite(Date.parse(value))) { + fail("manifest.createdAt is not a valid date-time"); + } } function validateOfficialTheme(value) { diff --git a/windows/assets/theme-package-validator.mjs b/windows/assets/theme-package-validator.mjs index 275fcfd1..654eeafe 100644 --- a/windows/assets/theme-package-validator.mjs +++ b/windows/assets/theme-package-validator.mjs @@ -79,7 +79,7 @@ const KEY_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/u; const PROVENANCE_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u; const COLOR_PATTERN = /^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|#[0-9a-fA-F]{3,4}|rgb\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*\)|rgba\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*(0|1|1\.0|0?\.[0-9]{1,6})\s*\))$/; -const RFC3339_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?(?:Z|[+-][0-9]{2}:[0-9]{2})$/; +const RFC3339_PATTERN = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]{1,9}))?(?:Z|([+-])([0-9]{2}):([0-9]{2}))$/; const OPEN_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0); const decoder = new TextDecoder("utf-8", { fatal: true }); const scriptPath = fileURLToPath(import.meta.url); @@ -265,7 +265,26 @@ async function sourceFileNames(root) { function validateTimestamp(value) { assertString(value, "manifest.createdAt", { min: 1, max: 40, pattern: RFC3339_PATTERN, controls: null }); - if (!Number.isFinite(Date.parse(value))) fail("manifest.createdAt is not a valid date-time"); + const match = RFC3339_PATTERN.exec(value); + if (!match) fail("manifest.createdAt is not a valid date-time"); + + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + const hour = Number(match[4]); + const minute = Number(match[5]); + const second = Number(match[6]); + const offsetHour = match[9] === undefined ? 0 : Number(match[9]); + const offsetMinute = match[10] === undefined ? 0 : Number(match[10]); + const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const calendarValid = month >= 1 && month <= 12 + && day >= 1 && day <= (daysInMonth[month - 1] ?? 0) + && hour <= 23 && minute <= 59 && second <= 59 + && offsetHour <= 23 && offsetMinute <= 59; + if (!calendarValid || !Number.isFinite(Date.parse(value))) { + fail("manifest.createdAt is not a valid date-time"); + } } function validateOfficialTheme(value) {