From edaed9adfa6eee5ff367c8eebd1759ffae5ba9c4 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 22 May 2026 08:55:57 -0400 Subject: [PATCH] LibJS: Protect DateParser from non-ASCII input DateParser is a GenericLexer, which is not particularly UTF-8 aware. Other engines do not accept ASCII input, so let's just reject non-ASCII strings outright. While we are here, let's avoid the std ctype header. It is explicitly undefined behavior to provide code points to isspace, isalpha, etc. that do not fit in an unsigned char. Although this isn't reachable any longer let's just nip it in the bud and use our safe AK character types. Just to demonstrate, the following returns different values for me on different systems and with different compilers: isdigit(0xff01) isalpha(0xff01) isspace(0xff01) --- Libraries/LibJS/Runtime/DateParser.h | 19 ++++++++++--------- .../builtins/Date/Date.parse.nonStandard.js | 10 ++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Libraries/LibJS/Runtime/DateParser.h b/Libraries/LibJS/Runtime/DateParser.h index cc34f58df6..7a474f5502 100644 --- a/Libraries/LibJS/Runtime/DateParser.h +++ b/Libraries/LibJS/Runtime/DateParser.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -15,8 +16,6 @@ #include #include -#include // Not included by default on macOS. - // Parse simplified ISO8601 and non-standard date formats to milliseconds // from epoch (double). Synopsis: // (1) Try to parse the string as simplified ISO8601 (case sensitive). @@ -50,6 +49,8 @@ class DateParser : public GenericLexer { public: ALWAYS_INLINE static double parse(StringView string) { + if (!string.is_ascii()) + return NAN; return DateParser(string).parse().value_or(NAN); } @@ -107,13 +108,13 @@ private: ParsedNumber result; for (; result.digits < MaxLength; ++result.digits) { - if (is_eof() || !next_is(isdigit)) + if (is_eof() || !next_is(is_ascii_digit)) return result; result.number *= 10; result.number += consume() - '0'; } - ignore_while(isdigit); + ignore_while(is_ascii_digit); return result; } @@ -331,7 +332,7 @@ private: case '(': ignore_until(')'); // Consume time zone name (Anything in brackets). ignore(); - ignore_while(isspace); + ignore_while(is_ascii_space); return true; } @@ -342,7 +343,7 @@ private: { VERIFY(m_hours.has_value()); - consume_while(isspace); + consume_while(is_ascii_space); if (consume_specific("AM"sv)) { if (!separator()) @@ -588,7 +589,7 @@ private: m_timezone_utc = true; - bool space = consume_while(isspace).length() > 0; + bool space = consume_while(is_ascii_space).length() > 0; switch (peek()) { case '+': case '-': @@ -622,7 +623,7 @@ private: if (str[i] != peek(i)) return false; - ignore_while(isalpha); // ... which can followed by anything. Just like Firefox and Chrome. + ignore_while(is_ascii_alpha); // ... which can followed by anything. Just like Firefox and Chrome. m_month = month; return separator(); // Must end with a separator. @@ -630,7 +631,7 @@ private: ALWAYS_INLINE bool word() // Alphanumeric strings that are not date "keywords". { - consume_while(isalpha); + consume_while(is_ascii_alpha); // Just like Firefox and Chrome: // - Ignore junk (bare words) at the beginning (before time or a date fragment has been read). // - Fail if a word is read later in the date string (exception: final time zone name, in brackets). diff --git a/Tests/LibJS/Runtime/builtins/Date/Date.parse.nonStandard.js b/Tests/LibJS/Runtime/builtins/Date/Date.parse.nonStandard.js index c12a381431..1b3ecf7cf1 100644 --- a/Tests/LibJS/Runtime/builtins/Date/Date.parse.nonStandard.js +++ b/Tests/LibJS/Runtime/builtins/Date/Date.parse.nonStandard.js @@ -698,3 +698,13 @@ test("multiple month names", () => { setTimeZone(originalTimeZone); }); + +test("non-ASCII input", () => { + expect(Date.parse("2024\uff01")).toBeNaN(); + expect(Date.parse("1234567\uff01")).toBeNaN(); + expect(Date.parse("Jan\uff01 01 2024")).toBeNaN(); + expect(Date.parse("hello\uff01 Jan 01 2024")).toBeNaN(); + expect(Date.parse("Jan 01 2024 12:34\u00a0PM")).toBeNaN(); + expect(Date.parse("Jan 01 2024 GMT\u00a0")).toBeNaN(); + expect(Date.parse("Jan 01 2024 (UTC)\u00a0")).toBeNaN(); +});