diff --git a/src/js/background.js b/src/js/background.js index 334f27bb9..21bad8901 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -183,6 +183,10 @@ const µBlock = (function() { // jshint ignore:line epickerEprom: null, scriptlets: {}, + + cspNoInlineScript: "script-src 'unsafe-eval' * blob: data:", + cspNoScripting: 'script-src http: https:', + cspNoInlineFont: 'font-src *', }; })(); diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 1343e4e32..6aa7ad90a 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -197,6 +197,72 @@ vAPI.SafeAnimationFrame.prototype = { /******************************************************************************/ /******************************************************************************/ +// https://github.com/uBlockOrigin/uBlock-issues/issues/552 +// Listen and report CSP violations so that blocked resources through CSP +// are properly reported in the logger. + +{ + const events = new Set(); + let timer; + + const send = function() { + vAPI.messaging.send( + 'scriptlets', + { + what: 'securityPolicyViolation', + type: 'net', + docURL: document.location.href, + violations: Array.from(events), + }, + response => { + if ( response === true ) { return; } + stop(); + } + ); + events.clear(); + }; + + const sendAsync = function() { + if ( timer !== undefined ) { return; } + timer = self.requestIdleCallback( + ( ) => { timer = undefined; send(); }, + { timeout: 2000 } + ); + }; + + const listener = function(ev) { + if ( ev.isTrusted !== true ) { return; } + if ( ev.disposition !== 'enforce' ) { return; } + events.add(JSON.stringify({ + url: ev.blockedURL || ev.blockedURI, + policy: ev.originalPolicy, + directive: ev.effectiveDirective || ev.violatedDirective, + })); + sendAsync(); + }; + + const stop = function() { + events.clear(); + if ( timer !== undefined ) { + self.cancelIdleCallback(timer); + timer = undefined; + } + document.removeEventListener('securitypolicyviolation', listener); + vAPI.shutdown.remove(stop); + }; + + document.addEventListener('securitypolicyviolation', listener); + vAPI.shutdown.add(stop); + + // We need to call at least once to find out whether we really need to + // listen to CSP violations. + sendAsync(); +} + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + vAPI.domWatcher = (function() { const addedNodeLists = []; @@ -343,7 +409,7 @@ vAPI.domWatcher = (function() { /******************************************************************************/ /******************************************************************************/ -vAPI.matchesProp = (function() { +vAPI.matchesProp = (( ) => { const docElem = document.documentElement; if ( typeof docElem.matches !== 'function' ) { if ( typeof docElem.mozMatchesSelector === 'function' ) { diff --git a/src/js/messaging.js b/src/js/messaging.js index c0ec034c8..ab3945c6a 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -567,11 +567,8 @@ var onMessage = function(request, sender, callback) { if ( µb.canInjectScriptletsNow === false ) { response.scriptlets = µb.scriptletFilteringEngine.retrieve(request); } - if ( µb.logger.enabled ) { - if ( response.noCosmeticFiltering !== true ) { - µb.logCosmeticFilters(tabId, frameId); - } - µb.logInlineScript(tabId, frameId); + if ( µb.logger.enabled && response.noCosmeticFiltering !== true ) { + µb.logCosmeticFilters(tabId, frameId); } break; @@ -1362,7 +1359,98 @@ const logCosmeticFilters = function(tabId, details) { /******************************************************************************/ -var onMessage = function(request, sender, callback) { +const logCSPViolations = function(pageStore, request) { + if ( µb.logger.enabled === false || pageStore === null ) { + return false; + } + if ( request.violations.length === 0 ) { + return true; + } + + const fctxt = µb.filteringContext.duplicate(); + fctxt.fromTabId(pageStore.tabId) + .setRealm('network') + .setDocOriginFromURL(request.docURL) + .setURL(request.docURL); + + let cspData = pageStore.extraData.get('cspData'); + if ( cspData === undefined ) { + cspData = new Map(); + + const policies = []; + const logData = []; + µb.staticNetFilteringEngine.matchAndFetchData( + 'csp', + request.docURL, + policies, + logData + ); + for ( let i = 0; i < policies.length; i++ ) { + cspData.set(policies[i], logData[i]); + } + + fctxt.type = 'inline-script'; + fctxt.filter = undefined; + if ( pageStore.filterRequest(fctxt) === 1 ) { + cspData.set(µb.cspNoInlineScript, fctxt.filter); + } + + fctxt.type = 'script'; + fctxt.filter = undefined; + if ( pageStore.filterScripting(fctxt, true) === 1 ) { + cspData.set(µb.cspNoScripting, fctxt.filter); + } + + fctxt.type = 'inline-font'; + fctxt.filter = undefined; + if ( pageStore.filterRequest(fctxt) === 1 ) { + cspData.set(µb.cspNoInlineFont, fctxt.filter); + } + + if ( cspData.size === 0 ) { return false; } + + pageStore.extraData.set('cspData', cspData); + } + + const typeMap = logCSPViolations.policyDirectiveToTypeMap; + for ( const json of request.violations ) { + const violation = JSON.parse(json); + let type = typeMap.get(violation.directive); + if ( type === undefined ) { continue; } + const logData = cspData.get(violation.policy); + if ( logData === undefined ) { continue; } + if ( /^[\w.+-]+:\/\//.test(violation.url) === false ) { + violation.url = request.docURL; + if ( type === 'script' ) { type = 'inline-script'; } + else if ( type === 'font' ) { type = 'inline-font'; } + } + fctxt.setURL(violation.url) + .setType(type) + .setFilter(logData) + .toLogger(); + } + + return true; +}; + +logCSPViolations.policyDirectiveToTypeMap = new Map([ + [ 'img-src', 'image' ], + [ 'connect-src', 'xmlhttprequest' ], + [ 'font-src', 'font' ], + [ 'frame-src', 'sub_frame' ], + [ 'media-src', 'media' ], + [ 'object-src', 'object' ], + [ 'script-src', 'script' ], + [ 'script-src-attr', 'script' ], + [ 'script-src-elem', 'script' ], + [ 'style-src', 'stylesheet' ], + [ 'style-src-attr', 'stylesheet' ], + [ 'style-src-elem', 'stylesheet' ], +]); + +/******************************************************************************/ + +const onMessage = function(request, sender, callback) { const tabId = sender && sender.tab ? sender.tab.id : 0; const pageStore = µb.pageStoreFromTabId(tabId); @@ -1373,7 +1461,7 @@ var onMessage = function(request, sender, callback) { } // Sync - var response; + let response; switch ( request.what ) { case 'domSurveyTransientReport': @@ -1411,6 +1499,10 @@ var onMessage = function(request, sender, callback) { logCosmeticFilters(tabId, request); break; + case 'securityPolicyViolation': + response = logCSPViolations(pageStore, request); + break; + case 'temporarilyAllowLargeMediaElement': if ( pageStore !== null ) { pageStore.allowLargeMediaElementsUntil = Date.now() + 2000; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 868f8b8ac..7f582d902 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -221,11 +221,12 @@ const pageStoreJunkyardMax = 10; /******************************************************************************/ const PageStore = function(tabId, context) { - this.init(tabId, context); + this.extraData = new Map(); this.journal = []; this.journalTimer = null; this.journalLastCommitted = this.journalLastUncommitted = undefined; this.journalLastUncommittedURL = undefined; + this.init(tabId, context); }; /******************************************************************************/ @@ -276,6 +277,7 @@ PageStore.prototype.init = function(tabId, context) { this.largeMediaTimer = null; this.netFilteringCache = NetFilteringResultCache.factory(); this.internalRedirectionCount = 0; + this.extraData.clear(); // The current filtering context is cloned because: // - We may be called with or without the current context having been diff --git a/src/js/scriptlets/inlinescript-logger.js b/src/js/scriptlets/inlinescript-logger.js deleted file mode 100644 index 0738033c3..000000000 --- a/src/js/scriptlets/inlinescript-logger.js +++ /dev/null @@ -1,68 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2018-present Raymond Hill - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see {http://www.gnu.org/licenses/}. - - Home: https://github.com/gorhill/uBlock -*/ - -'use strict'; - -/******************************************************************************/ - -// The purpose is to find out whether the current document make use of -// inline script tags, and if so report to the main process for logging -// purpose. - -(function() { - - if ( typeof vAPI !== 'object' ) { return; } - - if ( - document.querySelector('script:not([src])') === null && - document.querySelector('a[href^="javascript:"]') === null && - document.querySelector('[onabort],[onblur],[oncancel],[oncanplay],[oncanplaythrough],[onchange],[onclick],[onclose],[oncontextmenu],[oncuechange],[ondblclick],[ondrag],[ondragend],[ondragenter],[ondragexit],[ondragleave],[ondragover],[ondragstart],[ondrop],[ondurationchange],[onemptied],[onended],[onerror],[onfocus],[oninput],[oninvalid],[onkeydown],[onkeypress],[onkeyup],[onload],[onloadeddata],[onloadedmetadata],[onloadstart],[onmousedown],[onmouseenter],[onmouseleave],[onmousemove],[onmouseout],[onmouseover],[onmouseup],[onwheel],[onpause],[onplay],[onplaying],[onprogress],[onratechange],[onreset],[onresize],[onscroll],[onseeked],[onseeking],[onselect],[onshow],[onstalled],[onsubmit],[onsuspend],[ontimeupdate],[ontoggle],[onvolumechange],[onwaiting],[onafterprint],[onbeforeprint],[onbeforeunload],[onhashchange],[onlanguagechange],[onmessage],[onoffline],[ononline],[onpagehide],[onpageshow],[onrejectionhandled],[onpopstate],[onstorage],[onunhandledrejection],[onunload],[oncopy],[oncut],[onpaste]') === null - ) { - return; - } - - vAPI.messaging.send('scriptlets', { - what: 'inlinescriptFound', - docURL: window.location.href - }); - -})(); - - - - - - - - -/******************************************************************************* - - DO NOT: - - Remove the following code - - Add code beyond the following code - Reason: - - https://github.com/gorhill/uBlock/pull/3721 - - uBO never uses the return value from injected content scripts - -**/ - -void 0; - diff --git a/src/js/traffic.js b/src/js/traffic.js index 65a0048da..8450e74ce 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -773,7 +773,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { const builtinDirectives = []; if ( pageStore.filterScripting(fctxt, true) === 1 ) { - builtinDirectives.push("script-src http: https:"); + builtinDirectives.push(µBlock.cspNoScripting); if ( loggerEnabled ) { fctxt.setRealm('network').setType('scripting').toLogger(); } @@ -788,9 +788,9 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { fctxt2.setDocOriginFromURL(fctxt.url); const result = pageStore.filterRequest(fctxt2); if ( result === 1 ) { - builtinDirectives.push("script-src 'unsafe-eval' * blob: data:"); + builtinDirectives.push(µBlock.cspNoInlineScript); } - if ( result !== 0 && loggerEnabled ) { + if ( result === 2 && loggerEnabled ) { fctxt2.setRealm('network').toLogger(); } } @@ -799,14 +799,14 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { // - Use a CSP to also forbid inline fonts if remote fonts are blocked. fctxt.type = 'inline-font'; if ( pageStore.filterRequest(fctxt) === 1 ) { - builtinDirectives.push('font-src *'); + builtinDirectives.push(µBlock.cspNoInlineFont); if ( loggerEnabled ) { fctxt.setRealm('network').toLogger(); } } if ( builtinDirectives.length !== 0 ) { - cspSubsets[0] = builtinDirectives.join('; '); + cspSubsets[0] = builtinDirectives.join(', '); } // ======== filter-based policies diff --git a/src/js/ublock.js b/src/js/ublock.js index 3bdc64e52..1ecc34fbb 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -555,14 +555,6 @@ var matchBucket = function(url, hostname, bucket, start) { }); }; -µBlock.logInlineScript = function(tabId, frameId) { - vAPI.tabs.injectScript(tabId, { - frameId: frameId, - file: '/js/scriptlets/inlinescript-logger.js', - runAt: 'document_end' - }); -}; - /******************************************************************************/ µBlock.scriptlets = (function() {