Skip to content

Commit 64c1f87

Browse files
committed
Add ability to control trusted status of filter lists
Related discussion: uBlockOrigin/uBlock-issues#2895 Changes: The _content of the My filters_ pane is now considered untrusted by default, and only uBO's own lists are now trusted by default. It has been observed that too many people will readily copy-paste filters from random sources. Copy-pasting filters which require trust represents a security risk to users with no understanding of how the filters work and their potential abuse. Using a filter which requires trust in a filter list from an untrusted source will cause the filter to be invalid, i.e. shown as an error. A new advanced setting has been added to control which lists are considered trustworthy: `trustedListPrefixes`, which is a space- separated list of tokens. Examples of possible values: - `ublock-`: trust only uBO lists, exclude everything else including content of _My filters_ (default value) - `ublock- user-`: trust uBO lists and content of _My filters_ - `-`: trust no list, essentially disabling all filters requiring trust (admins or people who don't trust us may want to use this) One can also decide to trust lists maintained elsewhere. For example, for stock AdGuard lists add ` adguard-`. To trust stock EasyList lists, add ` easylist-`. To trust a specific regional stock list, look-up its token in assets.json and add to `trustedListPrefixes`. The matching is made with String.startsWith(), hence why `ublock-` matches all uBO's own filter lists. This also allows to trust imported lists, for example add ` https://filters.adtidy.org/extension/ublock/filters/` to trust all non-stock AdGuard lists. Add the complete URL of a given imported list to trust only that one list. URLs not starting with `https://` or `file:///` will be rejected, i.e. `http://example.org` will be ignored. Invalid URLs are rejected.
1 parent 801d569 commit 64c1f87

File tree

8 files changed

+114
-14
lines changed

8 files changed

+114
-14
lines changed

src/js/1p-filters.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ const cmEditor = new CodeMirror(qs$('#userFilters'), {
4848
styleActiveLine: {
4949
nonEmpty: true,
5050
},
51-
trustedSource: true,
5251
});
5352

5453
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
@@ -89,6 +88,12 @@ let cachedUserFilters = '';
8988
getHints();
9089
}
9190

91+
vAPI.messaging.send('dashboard', {
92+
what: 'getTrustedScriptletTokens',
93+
}).then(tokens => {
94+
cmEditor.setOption('trustedScriptletTokens', tokens);
95+
});
96+
9297
/******************************************************************************/
9398

9499
function getEditorText() {
@@ -167,6 +172,8 @@ async function renderUserFilters(merge = false) {
167172
});
168173
if ( details instanceof Object === false || details.error ) { return; }
169174

175+
cmEditor.setOption('trustedSource', details.trustedSource === true);
176+
170177
const newContent = details.content.trim();
171178

172179
if ( merge && self.hasUnsavedData() ) {

src/js/asset-viewer.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,20 @@ import './codemirror/ubo-static-filtering.js';
6868

6969
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
7070

71-
const hints = await vAPI.messaging.send('dashboard', {
71+
vAPI.messaging.send('dashboard', {
7272
what: 'getAutoCompleteDetails'
73-
});
74-
if ( hints instanceof Object ) {
73+
}).then(hints => {
74+
if ( hints instanceof Object === false ) { return; }
7575
const mode = cmEditor.getMode();
76-
if ( mode.setHints instanceof Function ) {
77-
mode.setHints(hints);
78-
}
79-
}
76+
if ( mode.setHints instanceof Function === false ) { return; }
77+
mode.setHints(hints);
78+
});
79+
80+
vAPI.messaging.send('dashboard', {
81+
what: 'getTrustedScriptletTokens',
82+
}).then(tokens => {
83+
cmEditor.setOption('trustedScriptletTokens', tokens);
84+
});
8085

8186
const details = await vAPI.messaging.send('default', {
8287
what : 'getAssetContent',

src/js/background.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const hiddenSettingsDefault = {
8282
selfieAfter: 2,
8383
strictBlockingBypassDuration: 120,
8484
toolbarWarningTimeout: 60,
85+
trustedListPrefixes: 'ublock-',
8586
uiPopupConfig: 'unset',
8687
uiStyles: 'unset',
8788
updateAssetBypassBrowserCache: false,
@@ -255,6 +256,7 @@ const µBlock = { // jshint ignore:line
255256

256257
liveBlockingProfiles: [],
257258
blockingProfileColorCache: new Map(),
259+
parsedTrustedListPrefixes: [],
258260
uiAccentStylesheet: '',
259261
};
260262

src/js/codemirror/ubo-static-filtering.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,32 @@ let hintHelperRegistered = false;
3939

4040
/******************************************************************************/
4141

42+
const trustedScriptletTokens = new Set();
4243
let trustedSource = false;
4344

44-
CodeMirror.defineOption('trustedSource', false, (cm, state) => {
45-
trustedSource = state;
45+
CodeMirror.defineOption('trustedSource', false, (cm, value) => {
46+
trustedSource = value;
4647
self.dispatchEvent(new Event('trustedSource'));
4748
});
4849

50+
CodeMirror.defineOption('trustedScriptletTokens', trustedScriptletTokens, (cm, tokens) => {
51+
if ( tokens === undefined || tokens === null ) { return; }
52+
if ( typeof tokens[Symbol.iterator] !== 'function' ) { return; }
53+
trustedScriptletTokens.clear();
54+
for ( const token of tokens ) {
55+
trustedScriptletTokens.add(token);
56+
}
57+
self.dispatchEvent(new Event('trustedScriptletTokens'));
58+
});
59+
4960
/******************************************************************************/
5061

5162
CodeMirror.defineMode('ubo-static-filtering', function() {
5263
const astParser = new sfp.AstFilterParser({
5364
interactive: true,
54-
trustedSource,
5565
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
66+
trustedSource,
67+
trustedScriptletTokens,
5668
});
5769
const astWalker = astParser.getWalker();
5870
let currentWalkerNode = 0;
@@ -218,6 +230,10 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
218230
astParser.options.trustedSource = trustedSource;
219231
});
220232

233+
self.addEventListener('trustedScriptletTokens', ( ) => {
234+
astParser.options.trustedScriptletTokens = trustedScriptletTokens;
235+
});
236+
221237
return {
222238
lineComment: '!',
223239
token: function(stream) {
@@ -679,6 +695,8 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
679695
const astParser = new sfp.AstFilterParser({
680696
interactive: true,
681697
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
698+
trustedSource,
699+
trustedScriptletTokens,
682700
});
683701

684702
const changeset = [];
@@ -715,6 +733,9 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
715733
case sfp.AST_ERROR_IF_TOKEN_UNKNOWN:
716734
msg = `${msg}: Unknown preparsing token`;
717735
break;
736+
case sfp.AST_ERROR_UNTRUSTED_SOURCE:
737+
msg = `${msg}: Filter requires trusted source`;
738+
break;
718739
default:
719740
if ( astParser.isCosmeticFilter() && astParser.result.error ) {
720741
msg = `${msg}: ${astParser.result.error}`;
@@ -994,6 +1015,10 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
9941015
astParser.options.trustedSource = trustedSource;
9951016
});
9961017

1018+
self.addEventListener('trustedScriptletTokens', ( ) => {
1019+
astParser.options.trustedScriptletTokens = trustedScriptletTokens;
1020+
});
1021+
9971022
CodeMirror.defineInitHook(cm => {
9981023
cm.on('changes', onChanges);
9991024
cm.on('beforeChange', onBeforeChanges);

src/js/messaging.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ const onMessage = function(request, sender, callback) {
293293
response = getDomainNames(request.targets);
294294
break;
295295

296+
case 'getTrustedScriptletTokens':
297+
response = redirectEngine.getTrustedScriptletTokens();
298+
break;
299+
296300
case 'getWhitelist':
297301
response = {
298302
whitelist: µb.arrayFromWhitelist(µb.netWhitelist),
@@ -1570,6 +1574,7 @@ const onMessage = function(request, sender, callback) {
15701574

15711575
case 'readUserFilters':
15721576
return µb.loadUserFilters().then(result => {
1577+
result.trustedSource = µb.isTrustedList(µb.userFiltersPath);
15731578
callback(result);
15741579
});
15751580

src/js/redirect-engine.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,26 @@ class RedirectEngine {
426426
});
427427
}
428428

429+
getTrustedScriptletTokens() {
430+
const out = [];
431+
const isTrustedScriptlet = entry => {
432+
if ( entry.requiresTrust !== true ) { return false; }
433+
if ( entry.warURL !== undefined ) { return false; }
434+
if ( typeof entry.data !== 'string' ) { return false; }
435+
if ( entry.name.endsWith('.js') === false ) { return false; }
436+
return true;
437+
};
438+
for ( const [ name, entry ] of this.resources ) {
439+
if ( isTrustedScriptlet(entry) === false ) { continue; }
440+
out.push(name.slice(0, -3));
441+
}
442+
for ( const [ alias, name ] of this.aliases ) {
443+
if ( out.includes(name.slice(0, -3)) === false ) { continue; }
444+
out.push(alias.slice(0, -3));
445+
}
446+
return out;
447+
}
448+
429449
selfieFromResources(storage) {
430450
storage.put(
431451
RESOURCES_SELFIE_NAME,

src/js/static-filtering-parser.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export const AST_ERROR_DOMAIN_NAME = 1 << iota++;
100100
export const AST_ERROR_OPTION_DUPLICATE = 1 << iota++;
101101
export const AST_ERROR_OPTION_UNKNOWN = 1 << iota++;
102102
export const AST_ERROR_IF_TOKEN_UNKNOWN = 1 << iota++;
103+
export const AST_ERROR_UNTRUSTED_SOURCE = 1 << iota++;
103104

104105
iota = 0;
105106
const NODE_RIGHT_INDEX = iota++;
@@ -2244,12 +2245,25 @@ export class AstFilterParser {
22442245
if ( (flags & NODE_FLAG_ERROR) !== 0 ) { continue; }
22452246
realBad = false;
22462247
switch ( type ) {
2247-
case NODE_TYPE_EXT_PATTERN_RESPONSEHEADER:
2248+
case NODE_TYPE_EXT_PATTERN_RESPONSEHEADER: {
22482249
const pattern = this.getNodeString(targetNode);
22492250
realBad =
22502251
pattern !== '' && removableHTTPHeaders.has(pattern) === false ||
22512252
pattern === '' && isException === false;
22522253
break;
2254+
}
2255+
case NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN: {
2256+
if ( this.interactive !== true ) { break; }
2257+
if ( isException ) { break; }
2258+
const { trustedSource, trustedScriptletTokens } = this.options;
2259+
if ( trustedScriptletTokens instanceof Set === false ) { break; }
2260+
const token = this.getNodeString(targetNode);
2261+
if ( trustedScriptletTokens.has(token) && trustedSource !== true ) {
2262+
this.astError = AST_ERROR_UNTRUSTED_SOURCE;
2263+
realBad = true;
2264+
}
2265+
break;
2266+
}
22532267
default:
22542268
break;
22552269
}
@@ -2338,6 +2352,7 @@ export class AstFilterParser {
23382352
parentBeg + details.argBeg,
23392353
parentBeg + tokenEnd
23402354
);
2355+
this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, next);
23412356
if ( details.failed ) {
23422357
this.addNodeFlags(next, NODE_FLAG_ERROR);
23432358
this.addFlags(AST_FLAG_HAS_ERROR);

src/js/storage.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,32 @@ import {
370370
/******************************************************************************/
371371

372372
µb.isTrustedList = function(assetKey) {
373-
if ( assetKey.startsWith('ublock-') ) { return true; }
374-
if ( assetKey === this.userFiltersPath ) { return true; }
373+
if ( this.parsedTrustedListPrefixes.length === 0 ) {
374+
this.parsedTrustedListPrefixes =
375+
µb.hiddenSettings.trustedListPrefixes.split(/ +/).map(prefix => {
376+
if ( prefix === '' ) { return; }
377+
if ( prefix.startsWith('http://') ) { return; }
378+
if ( prefix.startsWith('file:///') ) { return prefix; }
379+
if ( prefix.startsWith('https://') === false ) {
380+
return prefix.includes('://') ? undefined : prefix;
381+
}
382+
try {
383+
const url = new URL(prefix);
384+
if ( url.hostname.length > 0 ) { return url.href; }
385+
} catch(_) {
386+
}
387+
}).filter(prefix => prefix !== undefined);
388+
}
389+
for ( const prefix of this.parsedTrustedListPrefixes ) {
390+
if ( assetKey.startsWith(prefix) ) { return true; }
391+
}
375392
return false;
376393
};
377394

395+
µb.onEvent('hiddenSettingsChanged', ( ) => {
396+
µb.parsedTrustedListPrefixes = [];
397+
});
398+
378399
/******************************************************************************/
379400

380401
µb.loadSelectedFilterLists = async function() {

0 commit comments

Comments
 (0)