From b1a00145bd704b87369c680621f68123e70cef52 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 29 Nov 2024 10:13:39 -0500 Subject: [PATCH] Mitigate potentially delayed execution of scriptlets in Firefox Related issue: https://github.com/uBlockOrigin/uBlock-issues/issues/3452 Use blob-based injection only when direct injection fails because of a page's CSP. This is a mitigation until a better approach is devised. Such future better approach to investigate: - Use `MAIN` world injection supported by contentScript.register() since Firefox 128 - Investigate registering script to inject ahead of time thru some heuristic --- platform/chromium/vapi-background-ext.js | 52 ++++++++++---- platform/firefox/vapi-background-ext.js | 92 ++++++++++++++++++------ src/js/scriptlet-filtering.js | 32 +-------- 3 files changed, 111 insertions(+), 65 deletions(-) diff --git a/platform/chromium/vapi-background-ext.js b/platform/chromium/vapi-background-ext.js index 9ee69d819..acbdc3e9d 100644 --- a/platform/chromium/vapi-background-ext.js +++ b/platform/chromium/vapi-background-ext.js @@ -208,19 +208,43 @@ vAPI.prefetching = (( ) => { /******************************************************************************/ -vAPI.scriptletsInjector = ((doc, details) => { - let script; - try { - script = doc.createElement('script'); - script.appendChild(doc.createTextNode(details.scriptlets)); - (doc.head || doc.documentElement).appendChild(script); - self.uBO_scriptletsInjected = details.filters; - } catch (ex) { - } - if ( script ) { - script.remove(); - script.textContent = ''; - } -}).toString(); +vAPI.scriptletsInjector = (( ) => { + const parts = [ + '(', + function(details) { + if ( typeof self.uBO_scriptletsInjected === 'string' ) { return; } + const doc = document; + const { location } = doc; + if ( location === null ) { return; } + const { hostname } = location; + if ( hostname !== '' && details.hostname !== hostname ) { return; } + let script; + try { + script = doc.createElement('script'); + script.appendChild(doc.createTextNode(details.scriptlets)); + (doc.head || doc.documentElement).appendChild(script); + self.uBO_scriptletsInjected = details.filters; + } catch (ex) { + } + if ( script ) { + script.remove(); + script.textContent = ''; + } + return 0; + }.toString(), + ')(', + 'json-slot', + ');', + ]; + const jsonSlot = parts.indexOf('json-slot'); + return (hostname, details) => { + parts[jsonSlot] = JSON.stringify({ + hostname, + scriptlets: details.mainWorld, + filters: details.filters, + }); + return parts.join(''); + }; +})(); /******************************************************************************/ diff --git a/platform/firefox/vapi-background-ext.js b/platform/firefox/vapi-background-ext.js index 65420bc3f..5710224a3 100644 --- a/platform/firefox/vapi-background-ext.js +++ b/platform/firefox/vapi-background-ext.js @@ -351,25 +351,77 @@ vAPI.Net = class extends vAPI.Net { /******************************************************************************/ -vAPI.scriptletsInjector = ((doc, details) => { - let script, url; - try { - const blob = new self.Blob( - [ details.scriptlets ], - { type: 'text/javascript; charset=utf-8' } - ); - url = self.URL.createObjectURL(blob); - script = doc.createElement('script'); - script.async = false; - script.src = url; - (doc.head || doc.documentElement || doc).append(script); - self.uBO_scriptletsInjected = details.filters; - } catch (ex) { - } - if ( url ) { - if ( script ) { script.remove(); } - self.URL.revokeObjectURL(url); - } -}).toString(); +vAPI.scriptletsInjector = (( ) => { + const parts = [ + '(', + function(details) { + if ( typeof self.uBO_scriptletsInjected === 'string' ) { return; } + const doc = document; + const { location } = doc; + if ( location === null ) { return; } + const { hostname } = location; + if ( hostname !== '' && details.hostname !== hostname ) { return; } + // Use a page world sentinel to verify that execution was + // successful + const { sentinel } = details; + let script; + try { + const code = [ + `self['${sentinel}'] = true;`, + details.scriptlets, + ].join('\n'); + script = doc.createElement('script'); + script.appendChild(doc.createTextNode(code)); + (doc.head || doc.documentElement).appendChild(script); + } catch (ex) { + } + if ( script ) { + script.remove(); + script.textContent = ''; + script = undefined; + } + if ( self.wrappedJSObject[sentinel] ) { + delete self.wrappedJSObject[sentinel]; + self.uBO_scriptletsInjected = details.filters; + return 0; + } + // https://github.com/uBlockOrigin/uBlock-issues/issues/235 + // Fall back to blob injection if execution through direct + // injection failed + let url; + try { + const blob = new self.Blob( + [ details.scriptlets ], + { type: 'text/javascript; charset=utf-8' } + ); + url = self.URL.createObjectURL(blob); + script = doc.createElement('script'); + script.async = false; + script.src = url; + (doc.head || doc.documentElement || doc).append(script); + self.uBO_scriptletsInjected = details.filters; + } catch (ex) { + } + if ( url ) { + if ( script ) { script.remove(); } + self.URL.revokeObjectURL(url); + } + return 0; + }.toString(), + ')(', + 'json-slot', + ');', + ]; + const jsonSlot = parts.indexOf('json-slot'); + return (hostname, details) => { + parts[jsonSlot] = JSON.stringify({ + hostname, + scriptlets: details.mainWorld, + filters: details.filters, + sentinel: vAPI.generateSecret(3), + }); + return parts.join(''); + }; +})(); /******************************************************************************/ diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index 1cc6a959b..b7a061772 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -106,36 +106,6 @@ const contentScriptRegisterer = new (class { /******************************************************************************/ -const mainWorldInjector = (( ) => { - const parts = [ - '(', - function(injector, details) { - if ( typeof self.uBO_scriptletsInjected === 'string' ) { return; } - const doc = document; - if ( doc.location === null ) { return; } - const hostname = doc.location.hostname; - if ( hostname !== '' && details.hostname !== hostname ) { return; } - injector(doc, details); - return 0; - }.toString(), - ')(', - vAPI.scriptletsInjector, ', ', - 'json-slot', - ');', - ]; - const jsonSlot = parts.indexOf('json-slot'); - return { - assemble: function(hostname, details) { - parts[jsonSlot] = JSON.stringify({ - hostname, - scriptlets: details.mainWorld, - filters: details.filters, - }); - return parts.join(''); - }, - }; -})(); - const isolatedWorldInjector = (( ) => { const parts = [ '(', @@ -334,7 +304,7 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine { const contentScript = []; if ( scriptletDetails.mainWorld ) { - contentScript.push(mainWorldInjector.assemble(hostname, scriptletDetails)); + contentScript.push(vAPI.scriptletsInjector(hostname, scriptletDetails)); } if ( scriptletDetails.isolatedWorld ) { contentScript.push(isolatedWorldInjector.assemble(hostname, scriptletDetails));