diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index 31073408e25f3..f352b6869b4d6 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -48,7 +48,6 @@ const cmEditor = new CodeMirror(qs$('#userFilters'), { styleActiveLine: { nonEmpty: true, }, - trustedSource: true, }); uBlockDashboard.patchCodeMirrorEditor(cmEditor); @@ -89,6 +88,12 @@ let cachedUserFilters = ''; getHints(); } +vAPI.messaging.send('dashboard', { + what: 'getTrustedScriptletTokens', +}).then(tokens => { + cmEditor.setOption('trustedScriptletTokens', tokens); +}); + /******************************************************************************/ function getEditorText() { @@ -167,6 +172,8 @@ async function renderUserFilters(merge = false) { }); if ( details instanceof Object === false || details.error ) { return; } + cmEditor.setOption('trustedSource', details.trustedSource === true); + const newContent = details.content.trim(); if ( merge && self.hasUnsavedData() ) { diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index dd69b7da56ef4..a6cc9d09b307c 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -68,15 +68,20 @@ import './codemirror/ubo-static-filtering.js'; uBlockDashboard.patchCodeMirrorEditor(cmEditor); - const hints = await vAPI.messaging.send('dashboard', { + vAPI.messaging.send('dashboard', { what: 'getAutoCompleteDetails' - }); - if ( hints instanceof Object ) { + }).then(hints => { + if ( hints instanceof Object === false ) { return; } const mode = cmEditor.getMode(); - if ( mode.setHints instanceof Function ) { - mode.setHints(hints); - } - } + if ( mode.setHints instanceof Function === false ) { return; } + mode.setHints(hints); + }); + + vAPI.messaging.send('dashboard', { + what: 'getTrustedScriptletTokens', + }).then(tokens => { + cmEditor.setOption('trustedScriptletTokens', tokens); + }); const details = await vAPI.messaging.send('default', { what : 'getAssetContent', diff --git a/src/js/background.js b/src/js/background.js index bcbb6f10ce5ea..3975bf7b241f3 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -82,6 +82,7 @@ const hiddenSettingsDefault = { selfieAfter: 2, strictBlockingBypassDuration: 120, toolbarWarningTimeout: 60, + trustedListPrefixes: 'ublock-', uiPopupConfig: 'unset', uiStyles: 'unset', updateAssetBypassBrowserCache: false, @@ -255,6 +256,7 @@ const µBlock = { // jshint ignore:line liveBlockingProfiles: [], blockingProfileColorCache: new Map(), + parsedTrustedListPrefixes: [], uiAccentStylesheet: '', }; diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index c824e020612c9..7b92085f12bef 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -39,20 +39,32 @@ let hintHelperRegistered = false; /******************************************************************************/ +const trustedScriptletTokens = new Set(); let trustedSource = false; -CodeMirror.defineOption('trustedSource', false, (cm, state) => { - trustedSource = state; +CodeMirror.defineOption('trustedSource', false, (cm, value) => { + trustedSource = value; self.dispatchEvent(new Event('trustedSource')); }); +CodeMirror.defineOption('trustedScriptletTokens', trustedScriptletTokens, (cm, tokens) => { + if ( tokens === undefined || tokens === null ) { return; } + if ( typeof tokens[Symbol.iterator] !== 'function' ) { return; } + trustedScriptletTokens.clear(); + for ( const token of tokens ) { + trustedScriptletTokens.add(token); + } + self.dispatchEvent(new Event('trustedScriptletTokens')); +}); + /******************************************************************************/ CodeMirror.defineMode('ubo-static-filtering', function() { const astParser = new sfp.AstFilterParser({ interactive: true, - trustedSource, nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'), + trustedSource, + trustedScriptletTokens, }); const astWalker = astParser.getWalker(); let currentWalkerNode = 0; @@ -218,6 +230,10 @@ CodeMirror.defineMode('ubo-static-filtering', function() { astParser.options.trustedSource = trustedSource; }); + self.addEventListener('trustedScriptletTokens', ( ) => { + astParser.options.trustedScriptletTokens = trustedScriptletTokens; + }); + return { lineComment: '!', token: function(stream) { @@ -679,6 +695,8 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { const astParser = new sfp.AstFilterParser({ interactive: true, nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'), + trustedSource, + trustedScriptletTokens, }); const changeset = []; @@ -715,6 +733,9 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { case sfp.AST_ERROR_IF_TOKEN_UNKNOWN: msg = `${msg}: Unknown preparsing token`; break; + case sfp.AST_ERROR_UNTRUSTED_SOURCE: + msg = `${msg}: Filter requires trusted source`; + break; default: if ( astParser.isCosmeticFilter() && astParser.result.error ) { msg = `${msg}: ${astParser.result.error}`; @@ -994,6 +1015,10 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { astParser.options.trustedSource = trustedSource; }); + self.addEventListener('trustedScriptletTokens', ( ) => { + astParser.options.trustedScriptletTokens = trustedScriptletTokens; + }); + CodeMirror.defineInitHook(cm => { cm.on('changes', onChanges); cm.on('beforeChange', onBeforeChanges); diff --git a/src/js/messaging.js b/src/js/messaging.js index 62fcd93463867..0a4e0c078ef37 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -293,6 +293,10 @@ const onMessage = function(request, sender, callback) { response = getDomainNames(request.targets); break; + case 'getTrustedScriptletTokens': + response = redirectEngine.getTrustedScriptletTokens(); + break; + case 'getWhitelist': response = { whitelist: µb.arrayFromWhitelist(µb.netWhitelist), @@ -1570,6 +1574,7 @@ const onMessage = function(request, sender, callback) { case 'readUserFilters': return µb.loadUserFilters().then(result => { + result.trustedSource = µb.isTrustedList(µb.userFiltersPath); callback(result); }); diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 5503c38a7e387..3feec75be3fd8 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -426,6 +426,26 @@ class RedirectEngine { }); } + getTrustedScriptletTokens() { + const out = []; + const isTrustedScriptlet = entry => { + if ( entry.requiresTrust !== true ) { return false; } + if ( entry.warURL !== undefined ) { return false; } + if ( typeof entry.data !== 'string' ) { return false; } + if ( entry.name.endsWith('.js') === false ) { return false; } + return true; + }; + for ( const [ name, entry ] of this.resources ) { + if ( isTrustedScriptlet(entry) === false ) { continue; } + out.push(name.slice(0, -3)); + } + for ( const [ alias, name ] of this.aliases ) { + if ( out.includes(name.slice(0, -3)) === false ) { continue; } + out.push(alias.slice(0, -3)); + } + return out; + } + selfieFromResources(storage) { storage.put( RESOURCES_SELFIE_NAME, diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index f874b5e4942e2..15cef54fed8ee 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -100,6 +100,7 @@ export const AST_ERROR_DOMAIN_NAME = 1 << iota++; export const AST_ERROR_OPTION_DUPLICATE = 1 << iota++; export const AST_ERROR_OPTION_UNKNOWN = 1 << iota++; export const AST_ERROR_IF_TOKEN_UNKNOWN = 1 << iota++; +export const AST_ERROR_UNTRUSTED_SOURCE = 1 << iota++; iota = 0; const NODE_RIGHT_INDEX = iota++; @@ -2244,12 +2245,25 @@ export class AstFilterParser { if ( (flags & NODE_FLAG_ERROR) !== 0 ) { continue; } realBad = false; switch ( type ) { - case NODE_TYPE_EXT_PATTERN_RESPONSEHEADER: + case NODE_TYPE_EXT_PATTERN_RESPONSEHEADER: { const pattern = this.getNodeString(targetNode); realBad = pattern !== '' && removableHTTPHeaders.has(pattern) === false || pattern === '' && isException === false; break; + } + case NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN: { + if ( this.interactive !== true ) { break; } + if ( isException ) { break; } + const { trustedSource, trustedScriptletTokens } = this.options; + if ( trustedScriptletTokens instanceof Set === false ) { break; } + const token = this.getNodeString(targetNode); + if ( trustedScriptletTokens.has(token) && trustedSource !== true ) { + this.astError = AST_ERROR_UNTRUSTED_SOURCE; + realBad = true; + } + break; + } default: break; } @@ -2338,6 +2352,7 @@ export class AstFilterParser { parentBeg + details.argBeg, parentBeg + tokenEnd ); + this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, next); if ( details.failed ) { this.addNodeFlags(next, NODE_FLAG_ERROR); this.addFlags(AST_FLAG_HAS_ERROR); diff --git a/src/js/storage.js b/src/js/storage.js index 77c5ec39cd39a..a250a5e3e874a 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -370,11 +370,32 @@ import { /******************************************************************************/ µb.isTrustedList = function(assetKey) { - if ( assetKey.startsWith('ublock-') ) { return true; } - if ( assetKey === this.userFiltersPath ) { return true; } + if ( this.parsedTrustedListPrefixes.length === 0 ) { + this.parsedTrustedListPrefixes = + µb.hiddenSettings.trustedListPrefixes.split(/ +/).map(prefix => { + if ( prefix === '' ) { return; } + if ( prefix.startsWith('http://') ) { return; } + if ( prefix.startsWith('file:///') ) { return prefix; } + if ( prefix.startsWith('https://') === false ) { + return prefix.includes('://') ? undefined : prefix; + } + try { + const url = new URL(prefix); + if ( url.hostname.length > 0 ) { return url.href; } + } catch(_) { + } + }).filter(prefix => prefix !== undefined); + } + for ( const prefix of this.parsedTrustedListPrefixes ) { + if ( assetKey.startsWith(prefix) ) { return true; } + } return false; }; +µb.onEvent('hiddenSettingsChanged', ( ) => { + µb.parsedTrustedListPrefixes = []; +}); + /******************************************************************************/ µb.loadSelectedFilterLists = async function() {