Skip to content

Commit b1a0014

Browse files
committed
Mitigate potentially delayed execution of scriptlets in Firefox
Related issue: uBlockOrigin/uBlock-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
1 parent d686769 commit b1a0014

File tree

3 files changed

+111
-65
lines changed

3 files changed

+111
-65
lines changed

platform/chromium/vapi-background-ext.js

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,19 +208,43 @@ vAPI.prefetching = (( ) => {
208208

209209
/******************************************************************************/
210210

211-
vAPI.scriptletsInjector = ((doc, details) => {
212-
let script;
213-
try {
214-
script = doc.createElement('script');
215-
script.appendChild(doc.createTextNode(details.scriptlets));
216-
(doc.head || doc.documentElement).appendChild(script);
217-
self.uBO_scriptletsInjected = details.filters;
218-
} catch (ex) {
219-
}
220-
if ( script ) {
221-
script.remove();
222-
script.textContent = '';
223-
}
224-
}).toString();
211+
vAPI.scriptletsInjector = (( ) => {
212+
const parts = [
213+
'(',
214+
function(details) {
215+
if ( typeof self.uBO_scriptletsInjected === 'string' ) { return; }
216+
const doc = document;
217+
const { location } = doc;
218+
if ( location === null ) { return; }
219+
const { hostname } = location;
220+
if ( hostname !== '' && details.hostname !== hostname ) { return; }
221+
let script;
222+
try {
223+
script = doc.createElement('script');
224+
script.appendChild(doc.createTextNode(details.scriptlets));
225+
(doc.head || doc.documentElement).appendChild(script);
226+
self.uBO_scriptletsInjected = details.filters;
227+
} catch (ex) {
228+
}
229+
if ( script ) {
230+
script.remove();
231+
script.textContent = '';
232+
}
233+
return 0;
234+
}.toString(),
235+
')(',
236+
'json-slot',
237+
');',
238+
];
239+
const jsonSlot = parts.indexOf('json-slot');
240+
return (hostname, details) => {
241+
parts[jsonSlot] = JSON.stringify({
242+
hostname,
243+
scriptlets: details.mainWorld,
244+
filters: details.filters,
245+
});
246+
return parts.join('');
247+
};
248+
})();
225249

226250
/******************************************************************************/

platform/firefox/vapi-background-ext.js

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -351,25 +351,77 @@ vAPI.Net = class extends vAPI.Net {
351351

352352
/******************************************************************************/
353353

354-
vAPI.scriptletsInjector = ((doc, details) => {
355-
let script, url;
356-
try {
357-
const blob = new self.Blob(
358-
[ details.scriptlets ],
359-
{ type: 'text/javascript; charset=utf-8' }
360-
);
361-
url = self.URL.createObjectURL(blob);
362-
script = doc.createElement('script');
363-
script.async = false;
364-
script.src = url;
365-
(doc.head || doc.documentElement || doc).append(script);
366-
self.uBO_scriptletsInjected = details.filters;
367-
} catch (ex) {
368-
}
369-
if ( url ) {
370-
if ( script ) { script.remove(); }
371-
self.URL.revokeObjectURL(url);
372-
}
373-
}).toString();
354+
vAPI.scriptletsInjector = (( ) => {
355+
const parts = [
356+
'(',
357+
function(details) {
358+
if ( typeof self.uBO_scriptletsInjected === 'string' ) { return; }
359+
const doc = document;
360+
const { location } = doc;
361+
if ( location === null ) { return; }
362+
const { hostname } = location;
363+
if ( hostname !== '' && details.hostname !== hostname ) { return; }
364+
// Use a page world sentinel to verify that execution was
365+
// successful
366+
const { sentinel } = details;
367+
let script;
368+
try {
369+
const code = [
370+
`self['${sentinel}'] = true;`,
371+
details.scriptlets,
372+
].join('\n');
373+
script = doc.createElement('script');
374+
script.appendChild(doc.createTextNode(code));
375+
(doc.head || doc.documentElement).appendChild(script);
376+
} catch (ex) {
377+
}
378+
if ( script ) {
379+
script.remove();
380+
script.textContent = '';
381+
script = undefined;
382+
}
383+
if ( self.wrappedJSObject[sentinel] ) {
384+
delete self.wrappedJSObject[sentinel];
385+
self.uBO_scriptletsInjected = details.filters;
386+
return 0;
387+
}
388+
// https://github.com/uBlockOrigin/uBlock-issues/issues/235
389+
// Fall back to blob injection if execution through direct
390+
// injection failed
391+
let url;
392+
try {
393+
const blob = new self.Blob(
394+
[ details.scriptlets ],
395+
{ type: 'text/javascript; charset=utf-8' }
396+
);
397+
url = self.URL.createObjectURL(blob);
398+
script = doc.createElement('script');
399+
script.async = false;
400+
script.src = url;
401+
(doc.head || doc.documentElement || doc).append(script);
402+
self.uBO_scriptletsInjected = details.filters;
403+
} catch (ex) {
404+
}
405+
if ( url ) {
406+
if ( script ) { script.remove(); }
407+
self.URL.revokeObjectURL(url);
408+
}
409+
return 0;
410+
}.toString(),
411+
')(',
412+
'json-slot',
413+
');',
414+
];
415+
const jsonSlot = parts.indexOf('json-slot');
416+
return (hostname, details) => {
417+
parts[jsonSlot] = JSON.stringify({
418+
hostname,
419+
scriptlets: details.mainWorld,
420+
filters: details.filters,
421+
sentinel: vAPI.generateSecret(3),
422+
});
423+
return parts.join('');
424+
};
425+
})();
374426

375427
/******************************************************************************/

src/js/scriptlet-filtering.js

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -106,36 +106,6 @@ const contentScriptRegisterer = new (class {
106106

107107
/******************************************************************************/
108108

109-
const mainWorldInjector = (( ) => {
110-
const parts = [
111-
'(',
112-
function(injector, details) {
113-
if ( typeof self.uBO_scriptletsInjected === 'string' ) { return; }
114-
const doc = document;
115-
if ( doc.location === null ) { return; }
116-
const hostname = doc.location.hostname;
117-
if ( hostname !== '' && details.hostname !== hostname ) { return; }
118-
injector(doc, details);
119-
return 0;
120-
}.toString(),
121-
')(',
122-
vAPI.scriptletsInjector, ', ',
123-
'json-slot',
124-
');',
125-
];
126-
const jsonSlot = parts.indexOf('json-slot');
127-
return {
128-
assemble: function(hostname, details) {
129-
parts[jsonSlot] = JSON.stringify({
130-
hostname,
131-
scriptlets: details.mainWorld,
132-
filters: details.filters,
133-
});
134-
return parts.join('');
135-
},
136-
};
137-
})();
138-
139109
const isolatedWorldInjector = (( ) => {
140110
const parts = [
141111
'(',
@@ -334,7 +304,7 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine {
334304

335305
const contentScript = [];
336306
if ( scriptletDetails.mainWorld ) {
337-
contentScript.push(mainWorldInjector.assemble(hostname, scriptletDetails));
307+
contentScript.push(vAPI.scriptletsInjector(hostname, scriptletDetails));
338308
}
339309
if ( scriptletDetails.isolatedWorld ) {
340310
contentScript.push(isolatedWorldInjector.assemble(hostname, scriptletDetails));

0 commit comments

Comments
 (0)