CVE-2024-11695
Overview
Background
- `losslessDecodeURI`
- The
UrlbarInput.sys.mjshelper 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
vflag) 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.
\p{...} Unicode property classes so whole classes of invisible and directional characters are re-encoded.Attack Path
- 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.
- Lure the victim
The victim is induced to open the crafted URL, which loads and populates the address bar via
UrlbarInput. - 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. - 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.
- Harvest trust Believing they are on the legitimate origin, the victim enters credentials or otherwise trusts content served by the attacker.
Impact Assessment
Files Changed
browser/components/urlbar/UrlbarInput.sys.mjs
Audit Directions
- Hard-coded code-point enumerationsSearch other display and sanitization paths for regexes with explicit
\uXXXXranges instead of\p{...}property classes, since such lists drift out of date as Unicode adds code points. - Lookahead-based whitespace collapsingReview 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 inputAudit 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.
Patch
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;