Medium CVSS 7.3 firefox Spoofing 🔧 Commit mapped

Overview

Medium
Severity
7.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionA crafted URL containing specific Unicode characters could have hidden the true origin of the page, resulting in a potential spoofing attack.
ComponentCore
Bug ClassSpoofing
Tracker1952213
Fix commit231564e3275b (firefox) +9/-8
CWECWE-290
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedRenwa Hiwa
Disclosed2025-04-01

Background

Invisible / format characters
Code points that render as nothing or as whitespace (controls, bidi marks, zero-width, variation selectors, unassigned glyphs). Shown unescaped in the URL bar, they can hide or reorder the visible origin.
JS regex `/v` (unicodeSets) flag
Enables set operations ([A--B], \p{...} property escapes) and treats the pattern as operating on full Unicode scalar values rather than UTF-16 code units.
Surrogate pairs
Astral code points (> U+FFFF) are two UTF-16 units (high + low surrogate). A code-unit-based pattern must enumerate explicit surrogate ranges; a scalar-aware /v pattern uses \u{...} directly.

Root Cause Analysis

The display-sanitizing regexes in losslessDecodeURI were written partly in terms of UTF-16 surrogate code units and did not comprehensively cover the set of invisible/format characters, so certain crafted code points slipped through unescaped and were rendered in the address bar. The second replace call matched astral format/invisible characters by spelling out surrogate pairs (\ud804[\udcbd\udccd], \ud834[\udd73-\udd7a], [\udb40-\udb43][\udc00-\udfff], etc.) and an ad-hoc list of BMP code points. This approach is brittle: it both risks matching a lone surrogate rather than a full scalar value and omits whole categories of format/invisible characters. An attacker could therefore include specific Unicode characters in a URL that losslessDecodeURI failed to encode, letting invisible or deceptive glyphs render in the URL bar and hide the page’s true origin (advisory: Spoofing, CWE-290). The first replace also mixed a bare inside a /v set, which under unicodeSets semantics is fragile.

The patch rewrites both regexes to be scalar-value-aware and category-complete. The invisible-controls pattern switches literals to \u{...} braces (\u{0020}, \u{2800}, \u{FFFC}) so matching is on full scalar values. The format/invisible pattern is replaced with a /v set built on Unicode property escapes: [[\p{Format}--[\u{200C}\u{200D}]]...\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}] — i.e. encode all Format characters except the legitimately needed zero-width joiner/non-joiner (U+200C/U+200D), plus unassigned, private-use, tag characters, and specific UI-mimicking emoji — expressed directly in scalar values instead of surrogate enumerations. An added comment warns that the patterns are Unicode-aware via /v and must work with full scalar values, never raw surrogates. This closes the gap so the previously-unescaped deceptive characters are now percent-encoded for display.

Key insight
The URL-bar sanitizer matched invisible/format characters via hand-listed UTF-16 surrogate pairs and an incomplete code-point set, so some crafted Unicode characters were displayed unescaped; the fix rewrites the regexes as /v scalar-aware \p{Format}-based sets that cover the whole category, leaving no gap for deceptive glyphs to render.

Attack Path

  1. Attacker Crafts a URL whose path/host contains specific invisible or format Unicode characters (e.g. astral tag characters U+E0000–E0FFF, or format characters outside the old explicit list) positioned to mask the true origin.
  2. Navigation Lures the victim to the URL (link, redirect); Firefox loads the page and prepares the address-bar display string.
  3. Sanitizer gap losslessDecodeURI’s old regexes fail to encode the crafted characters, so they pass through to the rendered value (the diff proves these characters were previously unmatched).
  4. Deceptive render The invisible/zero-width/directional characters render in the URL bar so the displayed origin differs from the real one.
  5. Primitive Address-bar origin spoofing: the user sees a trusted-looking origin for a page the attacker controls.

Impact Assessment

This is a UI/display logic bug in the desktop address bar (parent-process chrome JavaScript), not a memory-safety issue — there is no memory sink in the diff. The gained capability is origin/URL spoofing: an attacker can make the location bar misrepresent the page’s true origin, undermining the primary trust indicator users rely on (phishing, consent/permission confusion). No code execution or data corruption results; impact is confined to user deception, consistent with the advisory’s “moderate” rating and CVSS C:L/I:L/A:L.

Files Changed

  • browser/components/urlbar/UrlbarInput.sys.mjs

Audit Directions

  • Other URL/display sanitizers
    Audit any other place that decodes URLs or filenames for display (downloads panel, page info, permission prompts, notification origins) for the same surrogate-pair / incomplete-list pattern.
  • `/v` regex correctness
    Review other /v or /u regexes in front-end code for stray raw surrogates or BMP-only assumptions that could leak astral characters.
  • Format-category completeness
    Where invisible characters are filtered, confirm \p{Format}/\p{Default_Ignorable_Code_Point}-based coverage rather than curated code-point lists, and re-check the ZWJ/ZWNJ (U+200C/U+200D) carve-out does not itself enable spoofing in RTL contexts.
diff --git a/browser/components/urlbar/UrlbarInput.sys.mjs b/browser/components/urlbar/UrlbarInput.sys.mjs
index d9a04c782c2..4b69136aa31 100644
--- a/browser/components/urlbar/UrlbarInput.sys.mjs
+++ b/browser/components/urlbar/UrlbarInput.sys.mjs
@@ -5065,6 +5065,10 @@ function losslessDecodeURI(aURI) {
     }
   }
 
+  // IMPORTANT: The following regular expressions are Unicode-aware due to /v.
+  // Avoid matching high or low surrogate pairs directly, always work with
+  // full Unicode scalar values.
+
   // Encode potentially invisible characters:
   //   U+0000-001F: C0/C1 control characters
   //   U+007F-009F: commands
@@ -5083,7 +5087,7 @@ function losslessDecodeURI(aURI) {
   // preserve them encoded.
   value = value.replace(
     // eslint-disable-next-line no-control-regex
-    /[[\p{Separator}--\u0020]\p{Control}\u2800\ufffc]|\u0020(?=[\p{Other}\p{Separator}])|\s$/gv,
+    /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv,
     encodeURIComponent
   );
 
@@ -5094,21 +5098,18 @@ function losslessDecodeURI(aURI) {
   // per bug 582186:
   //   U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
   //   U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
-  //   U+FFF0-FFFB, U+1D173-1D17A (U+D834 + DD73-DD7A),
-  //   U+E0000-E0FFF (U+DB40-DB43 + U+DC00-DFFF)
+  //   U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF
   // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
   //   U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
   // Other format characters in the Cf category that are unlikely to be rendered
   // usefully:
-  //   U+0600-0605, U+08E2, U+110BD (U+D804 + U+DCBD),
-  //   U+110CD (U+D804 + U+DCCD), U+13430-13438 (U+D80D + U+DC30-DC38),
-  //   U+1BCA0-1BCA3 (U+D82F + U+DCA0-DCA3)
+  //   U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3
   // Mimicking UI parts:
-  //   U+1F50F-1F513 (U+D83D + U+DD0F-DD13), U+1F6E1 (U+D83D + U+DEE1)
+  //   U+1F50F-1F513, U+1F6E1
   // Unassigned codepoints, sometimes shown as empty glyphs.
   value = value.replace(
     // eslint-disable-next-line no-misleading-character-class
-    /[\u00ad\u034f\u061c\u06dd\u070f\u115f\u1160\u17b4\u17b5\u180b-\u180e\u200b\u200e\u200f\u202a-\u202e\u2060-\u206f\u3164\u0600-\u0605\u08e2\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufffb\p{Unassigned}\p{Private_Use}]|\ud804[\udcbd\udccd]|\ud80d[\udc30-\udc38]|\ud82f[\udca0-\udca3]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]|\ud83d[\udd0f-\udd13\udee1]/gv,
+    /[[\p{Format}--[\u{200C}\u{200D}]]\u{034F}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180B}-\u{180D}\u{3164}\u{FE00}-\u{FE0F}\u{FFA0}\u{FFF0}-\u{FFFB}\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}]/gv,
     encodeURIComponent
   );
   return value;
Loading diff…