Medium CVSS 5.4 firefox Spoofing 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionA crafted URL containing Arabic script and whitespace characters could have hidden the true origin of the page, resulting in a potential spoofing attack.
ComponentCore
Bug ClassSpoofing
Tracker1925496
Fix commit4a15af511c62 (firefox) +7/-5
CWECWE-1021
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedRenwa Hiwa
Disclosed2024-11-26

Background

`losslessDecodeURI`
The UrlbarInput.sys.mjs helper that decodes a URI for display in the address bar while re-encoding characters that could be used to mislead the user.
Location bar spoofing
An attack where crafted characters in a URL make the address bar render a false origin, tricking the user about which site they are on.
`\p{...}` Unicode property escapes
Regular-expression classes (used with the v flag) that match by Unicode category such as \p{Separator}, \p{Control}, \p{Unassigned}, and \p{Private_Use} rather than by an explicit enumerated code-point list.
`v` (unicodeSets) regex flag
The JavaScript regexp flag enabling set operations like [[\p{Separator}-- ]] (difference) and required for the \p{...} classes used in the fix.

Root Cause Analysis

The display path in losslessDecodeURI sanitizes a decoded URL through two String.prototype.replace calls that re-encode dangerous characters, but both relied on hand-maintained, hard-coded code-point ranges. The whitespace-collapsing rule (?= )|\s$ only re-encoded a space when it was immediately followed by another literal space, so a single space followed by a control, separator, or bidirectional-formatting character (such as Arabic-script directional marks) survived unencoded and could be exploited to visually push or hide the real origin. In parallel, the second replace enumerated only known invisible/formatting code points and therefore missed unassigned and private-use code points that some fonts render as empty glyphs, giving another way to insert invisible padding. Because the enumerations were incomplete, the invariant “every character that can distort the displayed origin is re-encoded before display” was violated.

The fix rewrites both patterns to use Unicode property classes under the v flag — re-encoding a space whenever it precedes any \p{Other} or \p{Separator} character, and adding \p{Unassigned} and \p{Private_Use} to the invisible-character class — so entire categories of spoofing characters are covered rather than an ever-incomplete list.

Key insight
The core mistake was sanitizing displayed URLs against hand-enumerated code-point lists that both missed spaces adjacent to non-space separators/controls and omitted unassigned/private-use code points; the fix replaces the enumerations with category-based \p{...} Unicode property classes so whole classes of invisible and directional characters are re-encoded.

Attack Path

  1. Craft the URL The attacker builds a URL for a page they control that embeds Arabic-script and whitespace characters, e.g. a space followed by a bidirectional/separator or control character, to manipulate the rendered order and spacing of the address bar text.
  2. Lure the victim The victim is induced to open the crafted URL, which loads and populates the address bar via UrlbarInput.
  3. Bypass sanitization Inside losslessDecodeURI, the old regexes leave the single space (not followed by another space) and any unassigned/private-use padding characters undecoded, so they render as-is.
  4. Distort the displayed origin The surviving whitespace and directional characters push or mask parts of the URL, causing the location bar to show a false, trusted-looking origin.
  5. Harvest trust Believing they are on the legitimate origin, the victim enters credentials or otherwise trusts content served by the attacker.

Impact Assessment

A successful attacker gains address-bar origin spoofing in the browser chrome (the parent/UI process), letting a page they control appear to originate from a different, trusted site. The precondition is that the victim navigates to a specially crafted URL containing the right combination of Arabic-script, whitespace, and invisible/formatting characters; no code execution is obtained, so the impact is rated moderate and limited to user deception and phishing.

Files Changed

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

Audit Directions

  • Hard-coded code-point enumerations
    Search other display and sanitization paths for regexes with explicit \uXXXX ranges instead of \p{...} property classes, since such lists drift out of date as Unicode adds code points.
  • Lookahead-based whitespace collapsing
    Review any URL/text formatting that only re-encodes a character based on the single next character (e.g. (?= )), which misses adjacent control, separator, or bidirectional characters.
  • UI text derived from untrusted input
    Audit all address-bar, notification, and permission-prompt rendering that decodes attacker-influenced strings, checking that invisible, unassigned, private-use, and bidirectional characters are neutralized before display.
diff --git a/browser/components/urlbar/UrlbarInput.sys.mjs b/browser/components/urlbar/UrlbarInput.sys.mjs
index d046e637057..960c3e2fa85 100644
--- a/browser/components/urlbar/UrlbarInput.sys.mjs
+++ b/browser/components/urlbar/UrlbarInput.sys.mjs
@@ -4889,12 +4889,13 @@ function losslessDecodeURI(aURI) {
   // Encode all adjacent space chars (U+0020), to prevent spoofing attempts
   // where they would push part of the URL to overflow the location bar
   // (bug 1395508). A single space, or the last space if the are many, is
-  // preserved to maintain readability of certain urls. We only do this for the
-  // common space, because others may be eaten when copied to the clipboard, so
-  // it's safer to preserve them encoded.
+  // preserved to maintain readability of certain urls if it's not followed by a
+  // control or separator character. We only do this for the common space,
+  // because others may be eaten when copied to the clipboard,so it's safer to
+  // preserve them encoded.
   value = value.replace(
     // eslint-disable-next-line no-control-regex
-    /[\u0000-\u001f\u007f-\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u2800\u3000\ufffc]|[\r\n\t]|\u0020(?=\u0020)|\s$/g,
+    /[[\p{Separator}--\u0020]\p{Control}\u2800\ufffc]|\u0020(?=[\p{Other}\p{Separator}])|\s$/gv,
     encodeURIComponent
   );
 
@@ -4916,9 +4917,10 @@ function losslessDecodeURI(aURI) {
   //   U+1BCA0-1BCA3 (U+D82F + U+DCA0-DCA3)
   // Mimicking UI parts:
   //   U+1F50F-1F513 (U+D83D + U+DD0F-DD13), U+1F6E1 (U+D83D + U+DEE1)
+  // 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]|\ud804[\udcbd\udccd]|\ud80d[\udc30-\udc38]|\ud82f[\udca0-\udca3]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]|\ud83d[\udd0f-\udd13\udee1]/g,
+    /[\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,
     encodeURIComponent
   );
   return value;
Loading diff…