From eaedaf5b10d2f7857c6b77fbf7d4a80681d4d46c Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 15 Apr 2025 12:47:02 -0400 Subject: [PATCH] Fix regexes with potential catastrophic backtracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quoted email below was sent to ubo-security at raymondhill dot net: ===== Dear Raymond, I am writing to report a potential Regular Expression Denial of Service (ReDoS) vulnerability in the 1p-filters.js script of uBlock Origin. The vulnerability occurs due to the use of the regular expression /\s+$/, which is used to remove trailing whitespace. This issue can lead to a denial of service when processing strings with a large number of trailing spaces, potentially causing a browser to freeze. Affected file(s) js/1p-filters.js Vulnerable pattern(s) Lines 131 and 167: /\s+$/ Description of the issue The regular expression /\s+$/ is applied to remove trailing whitespace in user‑ provided content. However, when the content has a large number of spaces (e.g., ~100,000), this pattern causes excessive backtracking in the regular expression engine, resulting in performance degradation and UI freezing. This is a classic ReDoS attack vector. Steps to reproduce 1. Open the uBlock Origin dashboard and navigate to the My filters tab. 2. Run the following code in the browser's DevTools Console or as a bookmarklet. 3. Observe the UI freezing for several seconds or even longer, depending on the number of spaces used. PoC (Proof of Concept) /** * poc.js — triggers ReDoS in 1p-filters.js * Expected: <1 ms; Actual: several seconds – UI freeze */ (() => { const payload = " ".repeat(100000) + "!"; // 100,000 spaces + sentinel const run = () => { if (!window.cmEditor) { console.error("cmEditor not ready"); return; } // Inject payload into the editor cmEditor.setValue(payload); console.time("ReDoS"); // Call the vulnerable function (mirroring getEditorText) cmEditor.getValue().replace(/\s+$/, ''); // Alternatively, simulate a realistic user flow: // document.querySelector('#userFiltersApply').click(); console.timeEnd("ReDoS"); }; if (document.readyState === "complete") { run(); } else { window.addEventListener("load", run, { once: true }); } })(); Impact This issue can significantly degrade the user experience, causing the page to become unresponsive. If an attacker can inject this malicious string into the page (for example, through XSS or other attacks), it could lead to a denial of service (DoS). This vulnerability can be triggered repeatedly, causing the browser to hang indefinitely. Suggested fix The issue can be mitigated by replacing /\s+$/ with a more efficient solution, such as a look‑behind assertion /(?<=\S)\s+$/ (available in modern browsers) which ensures no backtracking occurs, or using trimEnd() for legacy support: // Example of using look-behind: cmEditor.setValue(text.replace(/(?<=\S)\s+$/, '') + '\n\n'); // Alternatively, using trimEnd(): cmEditor.setValue(text.trimEnd() + '\n\n'); Additional information If required, I am happy to assist in testing or provide more information. Please feel free to contact me for further clarification. Best regards, [redacted] ===== --- src/js/1p-filters.js | 4 ++-- src/js/arglist-parser.js | 4 ++-- src/js/static-filtering-parser.js | 4 ++-- src/js/whitelist.js | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index c4f7590df..7e20a30c2 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -111,12 +111,12 @@ function currentStateChanged() { } function getEditorText() { - const text = cmEditor.getValue().replace(/\s+$/, ''); + const text = cmEditor.getValue().trimEnd(); return text === '' ? text : `${text}\n`; } function setEditorText(text) { - cmEditor.setValue(text.replace(/\s+$/, '') + '\n\n'); + cmEditor.setValue(`${text.trimEnd()}\n\n`); } /******************************************************************************/ diff --git a/src/js/arglist-parser.js b/src/js/arglist-parser.js index d8200df58..708029e2d 100644 --- a/src/js/arglist-parser.js +++ b/src/js/arglist-parser.js @@ -32,7 +32,7 @@ export class ArglistParser { this.transform = false; this.failed = false; this.reWhitespaceStart = /^\s+/; - this.reWhitespaceEnd = /\s+$/; + this.reWhitespaceEnd = /(?:^|\S)(\s+)$/; this.reOddTrailingEscape = /(?:^|[^\\])(?:\\\\)*\\$/; this.reTrailingEscapeChars = /\\+$/; } @@ -90,7 +90,7 @@ export class ArglistParser { } rightWhitespaceCount(s) { const match = this.reWhitespaceEnd.exec(s); - return match === null ? 0 : match[0].length; + return match === null ? 0 : match[1].length; } indexOfNextArgSeparator(pattern, separatorCode) { this.argBeg = this.argEnd = separatorCode !== this.separatorCode diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 66d2853ae..a3f6e1cc5 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -785,7 +785,7 @@ export class AstFilterParser { this.selectorCompiler = new ExtSelectorCompiler(options); // Regexes this.reWhitespaceStart = /^\s+/; - this.reWhitespaceEnd = /\s+$/; + this.reWhitespaceEnd = /(?:^|\S)(\s+)$/; this.reCommentLine = /^(?:!|#\s|####|\[adblock)/i; this.reExtAnchor = /(#@?(?:\$\?|\$|%|\?)?#).{1,2}/; this.reInlineComment = /(?:\s+#).*?$/; @@ -2786,7 +2786,7 @@ export class AstFilterParser { rightWhitespaceCount(s) { const match = this.reWhitespaceEnd.exec(s); - return match === null ? 0 : match[0].length; + return match === null ? 0 : match[1].length; } nextCommaInCommaSeparatedListString(s, start) { diff --git a/src/js/whitelist.js b/src/js/whitelist.js index 6d9e951c1..371d25f99 100644 --- a/src/js/whitelist.js +++ b/src/js/whitelist.js @@ -99,12 +99,12 @@ uBlockDashboard.patchCodeMirrorEditor(cmEditor); /******************************************************************************/ function getEditorText() { - let text = cmEditor.getValue().replace(/\s+$/, ''); - return text === '' ? text : text + '\n'; + const text = cmEditor.getValue().trimEnd(); + return text === '' ? text : `${text}\n`; } function setEditorText(text) { - cmEditor.setValue(text.replace(/\s+$/, '') + '\n'); + cmEditor.setValue(`${text.trimEnd()}\n`); } /******************************************************************************/