Skip to content

Fork Sync: Update from parent repository#307

Open
github-actions[bot] wants to merge 27 commits intomasterfrom
mirror
Open

Fork Sync: Update from parent repository#307
github-actions[bot] wants to merge 27 commits intomasterfrom
mirror

Conversation

@github-actions
Copy link

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

New directive: `join:[separator][sub-directives]`

[separator] is an author-defined two-character string to be used to split
the following sub-directives string. The sub-directives are fed back into
the helper scriptlet to generate sub-content, which will be joined into
a single string. Example:

...##+js(trusted-prevent-fetch, propstomatch, join:--length:10-20--[some literal content]--length:80-100-- push['ads'])
Possibly will fix cases of no content scripts found in Safari.

Related issue:
uBlockOrigin/uAssets#30158 (comment)
@github-actions
Copy link
Author

[puLL-Merge] - brave/uBlock@307

Diff
diff --git src/js/resources/create-html.js src/js/resources/create-html.js
index 9b22976d113b1..1329b3da06207 100644
--- src/js/resources/create-html.js
+++ src/js/resources/create-html.js
@@ -71,15 +71,12 @@ function trustedCreateHTML(
     const duration = parseInt(durationStr, 10);
     const domParser = new DOMParser();
     const externalDoc = domParser.parseFromString(htmlStr, 'text/html');
-    const docFragment = new DocumentFragment();
-    const toRemove = [];
+    const toAppend = [];
     while ( externalDoc.body.firstChild !== null ) {
-        const imported = document.adoptNode(externalDoc.body.firstChild);
-        docFragment.appendChild(imported);
-        if ( isNaN(duration) ) { continue; }
-        toRemove.push(imported);
+        toAppend.push(document.adoptNode(externalDoc.body.firstChild));
     }
-    if ( docFragment.firstChild === null ) { return; }
+    if ( toAppend.length === 0 ) { return; }
+    const toRemove = [];
     const remove = ( ) => {
         for ( const node of toRemove ) {
             if ( node.parentNode === null ) { continue; }
@@ -87,10 +84,21 @@ function trustedCreateHTML(
         }
         safe.uboLog(logPrefix, 'Node(s) removed');
     };
+    const appendOne = (target, nodes) => {
+        for ( const node of nodes ) {
+            target.append(node);
+            if ( isNaN(duration) ) { continue; }
+            toRemove.push(node);
+        }
+    };
     const append = ( ) => {
-        const parent = document.querySelector(parentSelector);
-        if ( parent === null ) { return false; }
-        parent.append(docFragment);
+        const targets = document.querySelectorAll(parentSelector);
+        if ( targets.length === 0 ) { return false; }
+        const limit = Math.min(targets.length, extraArgs.limit || 1) - 1;
+        for ( let i = 0; i < limit; i++ ) {
+            appendOne(targets[i], toAppend.map(a => a.cloneNode(true)));
+        }
+        appendOne(targets[limit], toAppend);
         safe.uboLog(logPrefix, 'Node(s) appended');
         if ( toRemove.length === 0 ) { return true; }
         setTimeout(remove, duration);
diff --git src/js/resources/prevent-fetch.js src/js/resources/prevent-fetch.js
index 7a161ecc0a703..dbe096a5dbc7c 100644
--- src/js/resources/prevent-fetch.js
+++ src/js/resources/prevent-fetch.js
@@ -51,6 +51,7 @@ function preventFetchFn(
     const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
     const validResponseProps = {
         ok: [ false, true ],
+        status: [ 403 ],
         statusText: [ '', 'Not Found' ],
         type: [ 'basic', 'cors', 'default', 'error', 'opaque' ],
     };
diff --git src/js/resources/scriptlets.js src/js/resources/scriptlets.js
index 687aa69c1785c..9c888b3d7ca2e 100755
--- src/js/resources/scriptlets.js
+++ src/js/resources/scriptlets.js
@@ -1828,7 +1828,7 @@ function trustedClickElement(
             const pos2 = s2.indexOf('=');
             const key = pos2 !== -1 ? s2.slice(0, pos2).trim() : s2;
             const value = pos2 !== -1 ? s2.slice(pos2+1).trim() : '';
-            out.re = new RegExp(`^${this.escapeRegexChars(key)}=${this.escapeRegexChars(value)}`);
+            out.re = new RegExp(`^${safe.escapeRegexChars(key)}=${safe.escapeRegexChars(value)}`);
             return out;
         }).filter(details => details !== undefined);
         const allCookies = assertions.some(o => o.type === 'cookie')
diff --git src/js/resources/utils.js src/js/resources/utils.js
index 45c96ca32522a..52e7a9b450b24 100644
--- src/js/resources/utils.js
+++ src/js/resources/utils.js
@@ -220,6 +220,14 @@ export function generateContentFn(trusted, directive) {
             warXHR.send();
         }).catch(( ) => '');
     }
+    if ( directive.startsWith('join:') ) {
+        const parts = directive.slice(7)
+                .split(directive.slice(5, 7))
+                .map(a => generateContentFn(trusted, a));
+        return parts.some(a => a instanceof Promise)
+            ? Promise.all(parts).then(parts => parts.join(''))
+            : parts.join('');
+    }
     if ( trusted ) {
         return directive;
     }

Description

This PR makes several changes to uBlock's scriptlet resources:

  1. create-html.js: Refactors the trustedCreateHTML function to support appending HTML to multiple matching elements (via querySelectorAll instead of querySelector), with a configurable limit parameter in extraArgs. Nodes are cloned for all targets except the last one, which receives the original nodes.

  2. prevent-fetch.js: Adds status: [403] to the list of valid response properties that can be configured when preventing fetch requests, allowing scriptlets to simulate a 403 Forbidden response.

  3. scriptlets.js: Fixes a bug in trustedClickElement where this.escapeRegexChars was incorrectly used instead of safe.escapeRegexChars.

  4. utils.js: Adds a new join: directive to generateContentFn that allows joining multiple content generation directives together.

Possible Issues

  1. Off-by-one in join: directive parsing: The directive format appears to use join:XY... where X is a 2-character separator extracted via directive.slice(5, 7), and the content starts at index 7 via directive.slice(7). However, if the separator is meant to be a single character, the slicing at slice(5, 7) would grab 2 characters, and slice(7) would start after those 2 characters. This seems intentional (2-char delimiter), but the naming join: with a single colon at index 4 means index 5 starts the delimiter — this could be confusing and error-prone if users expect a single-character delimiter.

  2. extraArgs.limit default behavior: When extraArgs.limit is not provided, extraArgs.limit || 1 defaults to 1, preserving backward compatibility. However, if extraArgs itself is undefined/null, this would throw. The caller needs to ensure extraArgs is always an object.

  3. Clone depth for complex DOM nodes: cloneNode(true) performs a deep clone but does not clone event listeners or associated data. This is standard DOM behavior but worth noting — cloned nodes won't have any programmatically attached listeners from the original adopted nodes.

Security Hotspots

  1. generateContentFn with join: directive (utils.js): The join: directive recursively calls generateContentFn for each part. If this function is reachable with untrusted input, recursive/nested join: directives could potentially be used for resource exhaustion. However, this appears to be gated by filter list authors (trusted context).

  2. trustedCreateHTML multi-target injection (create-html.js): Expanding from single to multiple target injection increases the surface area for DOM manipulation. Since this is a "trusted" scriptlet, it should only be usable by trusted filter list authors.

Changes

Changes

  • src/js/resources/create-html.js: Replaced DocumentFragment-based single-target append with an array-based multi-target append. Added appendOne helper function. Uses querySelectorAll and extraArgs.limit to control how many matching elements receive the HTML. Clones nodes for all but the last target.

  • src/js/resources/prevent-fetch.js: Added status: [403] to validResponseProps, enabling simulation of HTTP 403 responses.

  • src/js/resources/scriptlets.js: Fixed bug changing this.escapeRegexChars to safe.escapeRegexChars in cookie assertion regex construction within trustedClickElement.

  • src/js/resources/utils.js: Added join: directive support in generateContentFn. Parses a 2-character separator from positions 5-7, splits the remainder, recursively generates content for each part, and joins results. Handles both sync and async (Promise) parts.

sequenceDiagram
    participant Page as Web Page
    participant Script as Scriptlet Engine
    participant DOM as Document DOM
    
    Note over Script: trustedCreateHTML flow
    
</details>

<!-- Generated by claude-opus-4-6 -->
Loading

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.

1 participant