Skip to content

Conversation

@github-actions
Copy link

@github-actions github-actions bot commented Nov 1, 2025

This PR was automatically created by a GitHub Action triggered by a cron schedule. Please review the changes and merge if appropriate.

MS store validator rejects those rules in static rulesets.
- Make place for more dynamic regex-based rules when there is a
  risk session regex-based rules could interfere

- Do not prune `allow` strict-block rules as they do not contribute
  toward the overall regex-based rule count

Possibly related issue:
uBlockOrigin/uBOL-home#556
@Scriptlet prevent-dialog

@description
Programmatically close `dialog` elements.

@param [selector]
Optional. The dialog element must matches `dialog{selector}` for the
prevention to take place.

@Usage:
example.com##+js(prevent-dialog)
@github-actions
Copy link
Author

[puLL-Merge] - brave/uBlock@301

Diff
diff --git src/js/redirect-resources.js src/js/redirect-resources.js
index f1dd5c27ded58..26cb0b6540bb4 100644
--- src/js/redirect-resources.js
+++ src/js/redirect-resources.js
@@ -190,4 +190,7 @@ export default new Map([
     [ 'scorecardresearch_beacon.js', {
         alias: 'scorecardresearch.com/beacon.js',
     } ],
+    [ 'sensors-analytics.js', {
+        data: 'text',
+    } ],
 ]);
diff --git src/js/resources/json-edit.js src/js/resources/json-edit.js
index e5d72c60772aa..a95bc8c63921c 100644
--- src/js/resources/json-edit.js
+++ src/js/resources/json-edit.js
@@ -193,6 +193,7 @@ function editInboundObjectFn(
     const argPos = parseInt(argPosRaw, 10);
     if ( isNaN(argPos) ) { return; }
     const getArgPos = args => {
+        if ( Array.isArray(args) === false ) { return; }
         if ( argPos >= 0 ) {
             if ( args.length <= argPos ) { return; }
             return argPos;
--- /dev/null
+++ src/js/resources/prevent-dialog.js
@@ -0,0 +1,72 @@
+/*******************************************************************************
+
+    uBlock Origin - a comprehensive, efficient content blocker
+    Copyright (C) 2025-present Raymond Hill
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see {http://www.gnu.org/licenses/}.
+
+    Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/**
+ * @scriptlet prevent-dialog
+ * 
+ * @description
+ * Programmatically close `dialog` elements.
+ * 
+ * @param [selector]
+ * Optional. The dialog element must matches `dialog{selector}` for the
+ * prevention to take place.
+ * 
+ * @usage:
+ * example.com##+js(prevent-dialog)
+ * 
+ * */
+
+export function preventDialog(
+    selector = '',
+) {
+    const safe = safeSelf();
+    const logPrefix = safe.makeLogPrefix('prevent-dialog', selector);
+    const prevent = ( ) => {
+        debouncer = undefined;
+        const elems = document.querySelectorAll(`dialog${selector}`);
+        for ( const elem of elems ) {
+            if ( typeof elem.close !== 'function' ) { continue; }
+            if ( elem.open === false ) { continue; }
+            elem.close();
+            safe.uboLog(logPrefix, 'Closed');
+        }
+    };
+    let debouncer;
+    const observer = new MutationObserver(( ) => {
+        if ( debouncer !== undefined ) { return; }
+        debouncer = requestAnimationFrame(prevent);
+    });
+    observer.observe(document, {
+        attributes: true,
+        childList: true,
+        subtree: true,
+    });
+}
+registerScriptlet(preventDialog, {
+    name: 'prevent-dialog.js',
+    dependencies: [
+        safeSelf,
+    ],
+});
diff --git src/js/resources/prevent-fetch.js src/js/resources/prevent-fetch.js
index ba2565a69b48b..5bf03e0818f95 100644
--- src/js/resources/prevent-fetch.js
+++ src/js/resources/prevent-fetch.js
@@ -20,7 +20,11 @@
 
 */
 
-import { generateContentFn } from './utils.js';
+import {
+    generateContentFn,
+    matchObjectPropertiesFn,
+    parsePropertiesToMatchFn,
+} from './utils.js';
 import { proxyApplyFn } from './proxy-apply.js';
 import { registerScriptlet } from './base.js';
 import { safeSelf } from './safe-self.js';
@@ -43,20 +47,7 @@ function preventFetchFn(
         responseType
     );
     const extraArgs = safe.getExtraArgs(Array.from(arguments), 4);
-    const needles = [];
-    for ( const condition of safe.String_split.call(propsToMatch, /\s+/) ) {
-        if ( condition === '' ) { continue; }
-        const pos = condition.indexOf(':');
-        let key, value;
-        if ( pos !== -1 ) {
-            key = condition.slice(0, pos);
-            value = condition.slice(pos + 1);
-        } else {
-            key = 'url';
-            value = condition;
-        }
-        needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) });
-    }
+    const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
     const validResponseProps = {
         ok: [ false, true ],
         statusText: [ '', 'Not Found' ],
@@ -84,43 +75,41 @@ function preventFetchFn(
             responseProps.type = { value: responseType };
         }
     }
+    const fetchProps = (src, out) => {
+        if ( typeof src !== 'object' || src === null ) { return; }
+        const props = [
+            'body', 'cache', 'credentials', 'duplex', 'headers',
+            'integrity', 'keepalive', 'method', 'mode', 'priority',
+            'redirect', 'referrer', 'referrerPolicy', 'signal',
+        ];
+        for ( const prop of props ) {
+            if ( src[prop] === undefined ) { continue; }
+            out[prop] = src[prop];
+        }
+    };
+    const fetchDetails = args => {
+        const out = {};
+        if ( args[0] instanceof self.Request ) {
+            out.url = `${args[0].url}`;
+            fetchProps(args[0], out);
+        } else {
+            out.url = `${args[0]}`;
+        }
+        fetchProps(args[1], out);
+        return out;
+    };
     proxyApplyFn('fetch', function fetch(context) {
         const { callArgs } = context;
-        const details = callArgs[0] instanceof self.Request
-            ? callArgs[0]
-            : Object.assign({ url: callArgs[0] }, callArgs[1]);
-        let proceed = true;
-        try {
-            const props = new Map();
-            for ( const prop in details ) {
-                let v = details[prop];
-                if ( typeof v !== 'string' ) {
-                    try { v = safe.JSON_stringify(v); }
-                    catch { }
-                }
-                if ( typeof v !== 'string' ) { continue; }
-                props.set(prop, v);
-            }
-            if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) {
-                const out = Array.from(props).map(a => `${a[0]}:${a[1]}`);
-                safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
-            }
-            if ( propsToMatch === '' && responseBody === '' ) {
-                return context.reflect();
-            }
-            proceed = needles.length === 0;
-            for ( const { key, pattern } of needles ) {
-                if (
-                    pattern.expect && props.has(key) === false ||
-                    safe.testPattern(pattern, props.get(key)) === false
-                ) {
-                    proceed = true;
-                    break;
-                }
-            }
-        } catch {
+        const details = fetchDetails(callArgs);
+        if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) {
+            const out = Array.from(Object.entries(details)).map(a => `${a[0]}:${a[1]}`);
+            safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
+        }
+        if ( propsToMatch === '' && responseBody === '' ) {
+            return context.reflect();
         }
-        if ( proceed ) {
+        const matched = matchObjectPropertiesFn(propNeedles, details);
+        if ( matched === undefined || matched.length === 0 ) {
             return context.reflect();
         }
         return Promise.resolve(generateContentFn(trusted, responseBody)).then(text => {
@@ -148,6 +137,8 @@ registerScriptlet(preventFetchFn, {
     name: 'prevent-fetch.fn',
     dependencies: [
         generateContentFn,
+        matchObjectPropertiesFn,
+        parsePropertiesToMatchFn,
         proxyApplyFn,
         safeSelf,
     ],
diff --git src/js/resources/replace-argument.js src/js/resources/replace-argument.js
index 62867cbf261d0..1a305389bf5ce 100644
--- src/js/resources/replace-argument.js
+++ src/js/resources/replace-argument.js
@@ -72,6 +72,7 @@ export function trustedReplaceArgument(
         const parsed = parseReplaceFn(argraw.slice(5));
         if ( parsed === undefined ) { return; }
         replacer = arg => `${arg}`.replace(replacer.re, replacer.replacement);
+        Object.assign(replacer, parsed);
     } else if ( argraw.startsWith('add:') ) {
         const delta = parseFloat(argraw.slice(4));
         if ( isNaN(delta) ) { return; }
diff --git src/js/resources/scriptlets.js src/js/resources/scriptlets.js
index 19fc628d51739..c935fdb9e92db 100755
--- src/js/resources/scriptlets.js
+++ src/js/resources/scriptlets.js
@@ -27,6 +27,7 @@ import './json-edit.js';
 import './json-prune.js';
 import './noeval.js';
 import './object-prune.js';
+import './prevent-dialog.js';
 import './prevent-fetch.js';
 import './prevent-innerHTML.js';
 import './prevent-settimeout.js';
@@ -1589,6 +1590,7 @@ builtinScriptlets.push({
     name: 'm3u-prune.js',
     fn: m3uPrune,
     dependencies: [
+        'proxy-apply.fn',
         'safe-self.fn',
     ],
 });
@@ -1704,28 +1706,30 @@ function m3uPrune(
         if ( arg instanceof Request ) { return arg.url; }
         return String(arg);
     };
-    const realFetch = self.fetch;
-    self.fetch = new Proxy(self.fetch, {
-        apply: function(target, thisArg, args) {
-            if ( reUrl.test(urlFromArg(args[0])) === false ) {
-                return Reflect.apply(target, thisArg, args);
-            }
-            return realFetch(...args).then(realResponse =>
-                realResponse.text().then(text => {
-                    const response = new Response(pruner(text), {
-                        status: realResponse.status,
-                        statusText: realResponse.statusText,
-                        headers: realResponse.headers,
-                    });
-                    if ( toLog.length !== 0 ) {
-                        toLog.unshift(logPrefix);
-                        safe.uboLog(toLog.join('\n'));
-                    }
-                    return response;
-                })
-            );
+    proxyApplyFn('fetch', async function fetch(context) {
+        const args = context.callArgs;
+        const fetchPromise = context.reflect();
+        if ( reUrl.test(urlFromArg(args[0])) === false ) { return fetchPromise; }
+        const responseBefore = await fetchPromise;
+        const responseClone = responseBefore.clone();
+        const textBefore = await responseClone.text();
+        const textAfter = pruner(textBefore);
+        if ( textAfter === textBefore ) { return responseBefore; }
+        const responseAfter = new Response(textAfter, {
+            status: responseBefore.status,
+            statusText: responseBefore.statusText,
+            headers: responseBefore.headers,
+        });
+        Object.defineProperties(responseAfter, {
+            url: { value: responseBefore.url },
+            type: { value: responseBefore.type },
+        });
+        if ( toLog.length !== 0 ) {
+            toLog.unshift(logPrefix);
+            safe.uboLog(toLog.join('\n'));
         }
-    });
+        return responseAfter;
+    })
     self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, {
         apply: async (target, thisArg, args) => {
             if ( reUrl.test(urlFromArg(args[1])) === false ) {
--- /dev/null
+++ src/web_accessible_resources/sensors-analytics.js
@@ -0,0 +1,32 @@
+/*******************************************************************************
+
+    uBlock Origin - a browser extension to block requests.
+    Copyright (C) 2025-present Raymond Hill
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see {http://www.gnu.org/licenses/}.
+
+    Home: https://github.com/gorhill/uBlock
+*/
+
+(function() {
+    'use strict';
+    const noopfn = function() {
+    };
+    window.sensorsDataAnalytic201505 = {
+        init: noopfn,
+        quick: noopfn,
+        register: noopfn,
+        track: noopfn,
+    };
+})();

Description

This PR adds and enhances several scriptlets for content blocking in uBlock Origin:

  1. New redirect resource: Adds sensors-analytics.js for blocking analytics tracking
  2. New scriptlet: prevent-dialog.js - automatically closes dialog elements matching a selector
  3. Bug fix: Adds array type checking in json-edit.js to prevent errors
  4. Enhancement: Refactors prevent-fetch.js to use common property matching utilities
  5. Bug fix: Adds missing property assignment in replace-argument.js
  6. Refactor: Updates m3u-prune.js to use the proxy-apply utility instead of direct Proxy implementation

The changes improve code maintainability by consolidating property matching logic and add new blocking capabilities for unwanted UI elements and analytics.

Possible Issues

  1. Race condition in prevent-dialog.js: The debouncer variable is checked and set without proper synchronization. If multiple mutations fire rapidly, there's a theoretical race where requestAnimationFrame could be called multiple times before the first callback executes.

  2. Missing error handling in m3u-prune.js: The refactored fetch handler uses async/await but doesn't have try-catch blocks. If text() or other operations fail, the error will propagate unhandled.

  3. Incomplete fetch details extraction: In prevent-fetch.js, the fetchDetails function only extracts specific properties. Custom or newer fetch properties might be missed in matching logic.

Security Hotspots

  1. Unvalidated selector in prevent-dialog.js (Medium Risk): The selector parameter is directly concatenated into a querySelectorAll call without sanitization (document.querySelectorAll(dialog${selector})). While querySelector syntax is generally safe from injection, malicious filter list authors could potentially craft selectors that cause DoS via extremely complex or expensive CSS selectors that hang the browser.

  2. Response cloning without size limits in m3u-prune.js (Low Risk): The code clones responses and reads their full text content without size checks (await responseClone.text()). While this is in the context of M3U playlists (typically small), a malicious site could serve extremely large responses to cause memory exhaustion.

Privacy Hotspots

  1. Comprehensive fetch monitoring in prevent-fetch.js (Medium Risk): The scriptlet logs detailed fetch information including URLs, headers, credentials, and other metadata when logLevel > 1. This could expose sensitive information in debug logs if users share them. While this is opt-in debugging functionality, the logged data should be sanitized to remove tokens, credentials, and PII.

  2. Dialog logging in prevent-dialog.js (Low Risk): The scriptlet logs when dialogs are closed but doesn't log the dialog selector or content. This is appropriate privacy-wise, but future enhancements should avoid logging dialog content which might contain user data.

Changes

Changes

src/js/redirect-resources.js

  • Added sensors-analytics.js as a new redirect resource with text data type

src/js/resources/json-edit.js

  • Added array type validation before accessing array properties to prevent runtime errors when args is not an array

src/js/resources/prevent-dialog.js (New File)

  • New scriptlet that programmatically closes dialog elements matching an optional selector
  • Uses MutationObserver to watch for DOM changes
  • Implements debouncing via requestAnimationFrame to avoid excessive processing
  • Queries for dialog${selector} elements and calls .close() on open dialogs

src/js/resources/prevent-fetch.js

  • Refactored property matching to use shared utilities (parsePropertiesToMatchFn, matchObjectPropertiesFn)
  • Simplified needle parsing logic by delegating to common functions
  • Extracted fetch details parsing into separate fetchDetails helper function
  • Improved handling of Request objects and init options
  • Cleaned up matching logic - now returns reflection when no match instead of complex proceed flag

src/js/resources/replace-argument.js

  • Fixed bug where parsed regex/replacement properties weren't assigned back to the replacer function object

src/js/resources/scriptlets.js

  • Added prevent-dialog.js import
  • Added `proxy-apply

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants