Medium CVSS 6.5 webkit OOB 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentWebCore Url-pattern
Bug ClassOOB
Tracker294550
Fix commitfa85413077ac (WebKit/WebKit) +2/-1
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedBig Bear
Disclosed2025-09-15

Background

URLPattern
A Web API for matching and parsing URLs against a pattern syntax that supports regex groups; its constructor tokenizes untrusted string input from JavaScript.
Tokenizer / m_index
URLPatternTokenizer walks the input codepoint by codepoint; m_index is the global scan cursor into m_input across the whole pattern.
regexPosition
A position tracked specifically while scanning inside a regex group, which is the correct cursor for reasoning about whether the current escape is the group’s / input’s last character.
Escape validation
A backslash must be followed by a character to escape; a trailing backslash with no following character is malformed and must raise an error before any read of the next codepoint.
TypeError surfacing
Tokenizer errors are returned as an ExceptionOr and thrown to JS as a TypeError; failing to set the error means malformed input is processed instead of rejected.

Root Cause Analysis

The patch touches Tokenizer::tokenize() in Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp, the tokenizer that parses URLPattern regex-group syntax as part of the URLPattern Web API. Inside the branch that handles a backslash escape character (m_codepoint == ‘\’), the code must detect the malformed case where the escape is the final character of the input, i.e. there is no character after the backslash to escape. The vulnerable check read if (m_index == m_input.length() - 1), comparing the tokenizer’s global scan index m_index against the last-index position.

The fix changes it to if (regexPosition == m_input.length() - 1), comparing the position tracked while scanning inside the regex group instead. The violated invariant is that the end-of-input / no-trailing-character check for an escape must use the cursor that actually points at the backslash within the current regex scan; m_index is the wrong cursor in this context, so the equality test fails to fire when the backslash truly is the last codepoint. When the check does not fire, tokenize() does not record the ‘No character is provided after escape.’ error and does not break, so parsing continues and the code advances past the backslash to read the (non-existent) escaped codepoint, indexing at or beyond m_input.length(). The added layout test new URLPattern('(\\') — an unterminated regex group whose last character is a lone backslash — is precisely the input where m_index and regexPosition diverge, so before the fix this input drove the tokenizer past the end of the string rather than throwing a TypeError.

The fix restores the invariant that a trailing escape is diagnosed as an error (surfaced as a TypeError to JS) instead of being consumed, preventing the out-of-bounds read of the input buffer and the resulting crash.

Key insight
A classic wrong-variable off-by-one: the escape end-of-input guard compared the global cursor m_index instead of the in-regex cursor regexPosition, so a pattern ending in a lone backslash slipped past validation and let the tokenizer index beyond the input buffer.

Attack Path

  1. Reach the URLPattern tokenizer From attacker-controlled JavaScript in a page, call the URLPattern constructor, e.g. new URLPattern(’(\’) or an equivalent pattern whose regex portion ends in a backslash.
  2. Enter the regex-group escape branch The tokenizer scans into the ‘(’ regex group and reaches the trailing backslash, entering the m_codepoint == ‘\’ branch that is supposed to validate a following character exists.
  3. Defeat the end-of-input check Because the check used m_index rather than regexPosition, the condition m_index == m_input.length() - 1 evaluates false for this input, so the ‘No character is provided after escape’ error is skipped and parsing does not break.
  4. Read past the buffer The tokenizer advances to consume the escaped character that does not exist, indexing at or beyond m_input.length() and reading out of bounds of the input string, corrupting parser state or dereferencing invalid memory.
  5. Crash the renderer The out-of-bounds access produces the unexpected Safari (WebContent) crash described in the advisory; no further primitive is established by the commit.

Impact Assessment

The primitive is an out-of-bounds read of the pattern input string driven entirely from a single JavaScript constructor call, and the advisory scopes the impact to an unexpected crash. Realistically this is a controlled crash / denial of service rather than a strong corruption primitive: an OOB read one or a few elements past a heap-allocated string can leak adjacent bytes into parser decisions in theory, but nothing in the diff demonstrates a controllable read length or a write, so escalation toward RCE is not established. The bug is confined to the WebContent (renderer) process that runs the URLPattern implementation and remains behind the WebContent sandbox.

Changed Functions

FunctionChangeNotes
Tokenizer::tokenize
Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp
modified In the backslash-escape handling, the end-of-input guard was fixed from comparing m_index to comparing regexPosition against m_input.length()-1, so a trailing escape is correctly diagnosed as an error instead of being consumed past the buffer end.
urlpattern-invalid-pattern test
LayoutTests/fast/url/urlpattern-invalid-pattern.html
modified Adds assert_throws_js(TypeError, () => new URLPattern('(\\')) to lock in that an unclosed group ending in a lone backslash throws rather than crashing.

Files Changed

  • LayoutTests/fast/url/urlpattern-invalid-pattern.html
  • Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp

Audit Directions

  • Audit every index used in URLPatternTokenizer
    In URLPatternTokenizer.cpp grep for m_index, regexPosition, and m_input.length() and verify each length/last-character comparison uses the cursor appropriate to its scanning context, especially other break/error branches inside tokenize().
  • Hunt sibling escape/EOF checks
    Search the URLPattern module (URLPatternParser, URLPatternComponent) for ‘\’ escape handling and any == length() - 1 or + 1 cursor advances that assume a following codepoint exists after an escape or delimiter.
  • Find length()-1 underflow patterns
    Grep across WebCore string/URL parsers for length() - 1 and length() - N comparisons where length could be zero or where the compared index is not the loop cursor, a recurring source of off-by-one OOB reads.
  • Fuzz new URL-adjacent parsers
    Differentially fuzz URLPattern and related recently-added parsers with truncated escapes, unbalanced groups, and trailing backslashes to surface variants of consume-past-end behavior before validation.

Original Bug Report

The reporter's bug is still restricted on the tracker.