diff --git a/src/js/background.js b/src/js/background.js index 774f772..96e82b9 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -110,7 +110,9 @@ return { }, clearBrowserCacheCycle: 0, - cspNoWorkerSrc: undefined, + cspNoInlineScript: undefined, + cspNoWorker: undefined, + cspReportURI: 'about:blank', updateAssetsEvery: 11 * oneDay + 1 * oneHour + 1 * oneMinute + 1 * oneSecond, firstUpdateAfter: 11 * oneMinute, nextUpdateAfter: 11 * oneHour, diff --git a/src/js/contentscript-start.js b/src/js/contentscript-start.js index cb35321..3b74e51 100644 --- a/src/js/contentscript-start.js +++ b/src/js/contentscript-start.js @@ -30,16 +30,68 @@ if ( typeof vAPI !== 'object' ) { return; } - window.addEventListener('securitypolicyviolation', function(ev) { + vAPI.reportedViolations = vAPI.reportedViolations || new Set(); + + var cspReportURI = 'about:blank'; + var reportedViolations = vAPI.reportedViolations; + + var handler = function(ev) { + if ( + ev.isTrusted !== true || + ev.originalPolicy.includes(cspReportURI) === false + ) { + return false; + } + + // Firefox and Chromium differs in how they fill the + // 'effectiveDirective' property. Need to normalize here. + var directive = ev.effectiveDirective; + if ( directive.startsWith('script-src') ) { + directive = 'script-src'; + } else if ( directive.startsWith('worker-src') ) { + directive = 'worker-src'; + } else if ( directive.startsWith('child-src') ) { + directive = 'worker-src'; + } else { + return false; + } + + var blockedURL; + try { + blockedURL = new URL(ev.blockedURI); + } catch(ex) { + } + blockedURL = blockedURL !== undefined ? blockedURL.href || '' : ''; + + // Avoid reporting same violations repeatedly. + var violationKey = (directive + ' ' + blockedURL).trim(); + if ( reportedViolations.has(violationKey) ) { + return true; + } + reportedViolations.add(violationKey); + vAPI.messaging.send( 'contentscript.js', { what: 'securityPolicyViolation', - policy: ev.originalPolicy, - blockedURI: ev.blockedURI, - documentURI: ev.documentURI + directive: directive, + blockedURI: blockedURL, + documentURI: ev.documentURI, + blocked: ev.disposition === 'enforce' } ); - }); + + return true; + }; + + document.addEventListener( + 'securitypolicyviolation', + function(ev) { + if ( !handler(ev) ) { return; } + ev.stopPropagation(); + ev.preventDefault(); + }, + true + ); })(); diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 8c591a3..eb0e4bb 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -407,14 +407,21 @@ var collapser = (function() { // Mind "on..." attributes. (function() { - vAPI.messaging.send('contentscript.js', { - what: 'contentScriptSummary', - locationURL: window.location.href, - inlineScript: - 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 - }); + if ( + vAPI.reportedViolations === undefined || + vAPI.reportedViolations.has('script-src') === false + ) { + if ( document.querySelector('script:not([src])') !== null ) { + vAPI.messaging.send('contentscript.js', { + what: 'securityPolicyViolation', + directive: 'script-src', + documentURI: window.location.href + }); + if ( vAPI.reportedViolations ) { + vAPI.reportedViolations.add('script-src'); + } + } + } collapser.addMany(document.querySelectorAll('img')); collapser.addIFrames(document.querySelectorAll('iframe')); diff --git a/src/js/messaging.js b/src/js/messaging.js index 28a8e52..c915203 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -391,32 +391,23 @@ var µm = µMatrix; /******************************************************************************/ -var contentScriptSummaryHandler = function(tabId, details) { - // TODO: Investigate "Error in response to tabs.executeScript: TypeError: - // Cannot read property 'locationURL' of null" (2013-11-12). When can this - // happens? - if ( !details || !details.locationURL ) { return; } - - // scripts - if ( details.inlineScript !== true ) { - return; - } - - // https://github.com/gorhill/httpswitchboard/issues/25 - var pageStore = µm.pageStoreFromTabId(tabId); +var contentScriptSummaryHandler = function(tabId, pageStore, details) { if ( pageStore === null ) { return; } var pageHostname = pageStore.pageHostname; - var µmuri = µm.URI.set(details.locationURL); + var µmuri = µm.URI.set(details.documentURI); var frameURL = µmuri.normalizedURI(); - var frameHostname = µmuri.hostname; + + var blocked = details.blocked; + if ( blocked === undefined ) { + blocked = µm.mustBlock(pageHostname, µmuri.hostname, 'script'); + } // https://github.com/gorhill/httpswitchboard/issues/333 // Look-up here whether inline scripting is blocked for the frame. - var inlineScriptBlocked = µm.mustBlock(pageHostname, frameHostname, 'script'); var url = frameURL + '{inline_script}'; - pageStore.recordRequest('script', url, inlineScriptBlocked); - µm.logger.writeOne(tabId, 'net', pageHostname, url, 'script', inlineScriptBlocked); + pageStore.recordRequest('script', url, blocked); + µm.logger.writeOne(tabId, 'net', pageHostname, url, 'script', blocked); // https://github.com/gorhill/uMatrix/issues/225 // A good place to force an update of the page title, as at this point @@ -544,16 +535,19 @@ var onMessage = function(request, sender, callback) { break; case 'securityPolicyViolation': - if ( request.policy !== µm.cspNoWorkerSrc ) { break; } - var url = µm.URI.hostnameFromURI(request.blockedURI) !== '' ? - request.blockedURI : - request.documentURI; - if ( pageStore !== null ) { - pageStore.hasWebWorkers = true; - pageStore.recordRequest('script', url, true); - } - if ( tabContext !== null ) { - µm.logger.writeOne(tabId, 'net', rootHostname, url, 'worker', true); + if ( request.directive === 'worker-src' ) { + var url = µm.URI.hostnameFromURI(request.blockedURI) !== '' ? + request.blockedURI : + request.documentURI; + if ( pageStore !== null ) { + pageStore.hasWebWorkers = true; + pageStore.recordRequest('script', url, true); + } + if ( tabContext !== null ) { + µm.logger.writeOne(tabId, 'net', rootHostname, url, 'worker', request.blocked); + } + } else if ( request.directive === 'script-src' ) { + contentScriptSummaryHandler(tabId, pageStore, request); } break; diff --git a/src/js/traffic.js b/src/js/traffic.js index 6fd1bca..b88fb35 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -299,8 +299,17 @@ var onHeadersReceived = function(details) { var tabContext = µm.tabContextManager.lookup(tabId); if ( tabContext === null ) { return; } - var csp = []; + var csp = [], + cspReport = []; + // If javascript is not allowed, say so through a `Content-Security-Policy` + // directive. + // We block only inline-script tags, all the external javascript will be + // blocked by our request handler. + if ( µm.cspNoInlineScript === undefined ) { + µm.cspNoInlineScript = + "script-src 'unsafe-eval' blob: *;report-uri " + µm.cspReportURI; + } if ( µm.mustAllow( tabContext.rootHostname, @@ -308,39 +317,53 @@ var onHeadersReceived = function(details) { 'script' ) !== true ) { - csp.push("script-src 'unsafe-eval' blob: *"); + csp.push(µm.cspNoInlineScript); + } else { + cspReport.push(µm.cspNoInlineScript); } - if ( µm.cspNoWorkerSrc === undefined ) { - µm.cspNoWorkerSrc = vAPI.webextFlavor.startsWith('Mozilla-') ? - "child-src 'none'; frame-src data: blob: *" : - "worker-src 'none'" ; + // TODO: Firefox will eventually support `worker-src`: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1231788 + if ( µm.cspNoWorker === undefined ) { + µm.cspNoWorker = vAPI.webextFlavor.startsWith('Mozilla-') ? + "child-src 'none'; frame-src data: blob: *;report-uri " : + "worker-src 'none';report-uri " ; + µm.cspNoWorker += µm.cspReportURI; } if ( µm.tMatrix.evaluateSwitchZ('no-workers', tabContext.rootHostname) ) { - csp.push(µm.cspNoWorkerSrc); + csp.push(µm.cspNoWorker); + } else { + cspReport.push(µm.cspNoWorker); } - if ( csp.length === 0 ) { return; } - - // If javascript is not allowed, say so through a `Content-Security-Policy` - // directive. - // We block only inline-script tags, all the external javascript will be - // blocked by our request handler. + var headers = details.responseHeaders, + cspDirectives, i; - var cspDirectives = csp.join(','), - headers = details.responseHeaders, + if ( csp.length !== 0 ) { + cspDirectives = csp.join(','); i = headerIndexFromName('content-security-policy', headers); - // A CSP header is already present: just add our own directive as a - // separate disposition (i.e. use comma). - if ( i !== -1 ) { - headers[i].value += ',' + cspDirectives; - } else { - headers.push({ name: 'Content-Security-Policy', value: cspDirectives }); + if ( i !== -1 ) { + headers[i].value += ',' + cspDirectives; + } else { + headers.push({ name: 'Content-Security-Policy', value: cspDirectives }); + } + if ( requestType === 'doc' ) { + µm.logger.writeOne(tabId, 'net', '', cspDirectives, 'CSP', false); + } } - if ( requestType === 'doc' ) { - µm.logger.writeOne(tabId, 'net', '', csp, 'CSP', false); + if ( cspReport.length !== 0 ) { + cspDirectives = cspReport.join(','); + i = headerIndexFromName('content-security-policy-report-only', headers); + if ( i !== -1 ) { + headers[i].value += ',' + cspDirectives; + } else { + headers.push({ + name: 'Content-Security-Policy-Report-Only', + value: cspDirectives + }); + } } return { responseHeaders: headers };