fix #1936: ability to foil WebSocket using a CSP directive

This commit is contained in:
gorhill 2016-08-27 11:08:56 -04:00
parent 48dcca0250
commit 8586aee848

View file

@ -393,11 +393,10 @@ var onHeadersReceived = function(details) {
/******************************************************************************/ /******************************************************************************/
var onRootFrameHeadersReceived = function(details) { var onRootFrameHeadersReceived = function(details) {
var µb = µBlock; var µb = µBlock,
var tabId = details.tabId; tabId = details.tabId;
var requestURL = details.url;
µb.tabContextManager.push(tabId, requestURL); µb.tabContextManager.push(tabId, details.url);
// Lookup the page store associated with this tab id. // Lookup the page store associated with this tab id.
var pageStore = µb.pageStoreFromTabId(tabId); var pageStore = µb.pageStoreFromTabId(tabId);
@ -406,68 +405,60 @@ var onRootFrameHeadersReceived = function(details) {
} }
// I can't think of how pageStore could be null at this point. // I can't think of how pageStore could be null at this point.
var context = pageStore.createContextFromPage(); return processCSP(details, pageStore, pageStore.createContextFromPage());
context.requestURL = requestURL;
context.requestHostname = µb.URI.hostnameFromURI(requestURL);
context.requestType = 'inline-script';
var result = pageStore.filterRequestNoCache(context);
pageStore.logRequest(context, result);
if ( µb.logger.isEnabled() ) {
µb.logger.writeOne(
tabId,
'net',
result,
'inline-script',
requestURL,
context.rootHostname,
context.pageHostname
);
}
context.dispose();
// Don't block
if ( µb.isAllowResult(result) ) {
return;
}
µb.updateBadgeAsync(tabId);
return { 'responseHeaders': foilInlineScripts(details.responseHeaders) };
}; };
/******************************************************************************/ /******************************************************************************/
var onFrameHeadersReceived = function(details) { var onFrameHeadersReceived = function(details) {
var µb = µBlock;
var tabId = details.tabId;
// Lookup the page store associated with this tab id. // Lookup the page store associated with this tab id.
var pageStore = µb.pageStoreFromTabId(tabId); var pageStore = µBlock.pageStoreFromTabId(details.tabId);
if ( !pageStore ) { if ( !pageStore ) {
return; return;
} }
// Frame id of frame request is their own id, while the request is made // Frame id of frame request is their own id, while the request is made
// in the context of the parent. // in the context of the parent.
var context = pageStore.createContextFromFrameId(details.parentFrameId); return processCSP(
var requestURL = details.url; details,
pageStore,
pageStore.createContextFromFrameId(details.parentFrameId)
);
};
/******************************************************************************/
var processCSP = function(details, pageStore, context) {
var µb = µBlock,
tabId = details.tabId,
requestURL = details.url,
loggerEnabled = µb.logger.isEnabled();
context.requestURL = requestURL; context.requestURL = requestURL;
context.requestHostname = µb.URI.hostnameFromURI(requestURL); context.requestHostname = µb.URI.hostnameFromURI(requestURL);
context.requestType = 'inline-script'; context.requestType = 'inline-script';
var inlineScriptResult = pageStore.filterRequestNoCache(context),
blockInlineScript = µb.isBlockResult(inlineScriptResult);
var result = pageStore.filterRequestNoCache(context); context.requestType = 'websocket';
var websocketResult = pageStore.filterRequestNoCache(context),
blockWebsocket = µb.isBlockResult(websocketResult);
pageStore.logRequest(context, result); var headersChanged = false;
if ( blockInlineScript || blockWebsocket ) {
headersChanged = foilWithCSP(
details.responseHeaders,
blockInlineScript,
blockWebsocket
);
}
if ( µb.logger.isEnabled() ) { if ( loggerEnabled ) {
µb.logger.writeOne( µb.logger.writeOne(
tabId, tabId,
'net', 'net',
result, inlineScriptResult,
'inline-script', 'inline-script',
requestURL, requestURL,
context.rootHostname, context.rootHostname,
@ -475,16 +466,27 @@ var onFrameHeadersReceived = function(details) {
); );
} }
if ( loggerEnabled && blockWebsocket ) {
µb.logger.writeOne(
tabId,
'net',
websocketResult,
'websocket',
requestURL,
context.rootHostname,
context.pageHostname
);
}
context.dispose(); context.dispose();
// Don't block if ( headersChanged !== true ) {
if ( µb.isAllowResult(result) ) {
return; return;
} }
µb.updateBadgeAsync(tabId); µb.updateBadgeAsync(tabId);
return { 'responseHeaders': foilInlineScripts(details.responseHeaders) }; return { 'responseHeaders': details.responseHeaders };
}; };
/******************************************************************************/ /******************************************************************************/
@ -536,98 +538,106 @@ var foilLargeMediaElement = function(details) {
/******************************************************************************/ /******************************************************************************/
var foilInlineScripts = function(headers) { var foilWithCSP = function(headers, noInlineScript, noWebsocket) {
// Below is copy-pasta from uMatrix's project. var i = headerIndexFromName('content-security-policy', headers),
before = i === -1 ? '' : headers[i].value.trim(),
after = before;
// If javascript is not allowed, say so through a `Content-Security-Policy` if ( noInlineScript ) {
// directive. after = foilWithCSPDirective(
// We block only inline-script tags, all the external javascript will be after,
// blocked by our request handler. /script-src[^;]*;?\s*/,
"script-src 'unsafe-eval' *",
// https://github.com/gorhill/uMatrix/issues/129 /'unsafe-inline'\s*|'nonce-[^']+'\s*/g
// https://github.com/gorhill/uMatrix/issues/320 );
// Modernize CSP injection:
// - Do not overwrite blindly possibly already present CSP header
// - Add CSP directive to block inline script ONLY if needed
// - If we end up modifying an existing CSP, strip out `report-uri`
// to prevent spurious CSP violations.
// Is there a CSP header present?
// If not, inject a script-src CSP directive to prevent inline javascript
// from executing.
var i = headerIndexFromName('content-security-policy', headers);
if ( i === -1 ) {
headers.push({
'name': 'Content-Security-Policy',
'value': "script-src 'unsafe-eval' *"
});
return headers;
} }
// A CSP header is already present. if ( noWebsocket ) {
// Remove the CSP header, we will re-inject it after processing it. after = foilWithCSPDirective(
// TODO: We are currently forced to add the CSP header at the end of the after,
// headers array, because this is what the platform specific code /connect-src[^;]*;?\s*/,
// expect (Firefox). 'connect-src http:',
var csp = headers.splice(i, 1)[0].value.trim(); /wss?:[^\s]*\s*/g
);
// Is there a script-src directive in the CSP header?
// If not, we simply need to append our script-src directive.
// https://github.com/gorhill/uMatrix/issues/320
// Since we are modifying an existing CSP header, we need to strip out
// 'report-uri' if it is present, to prevent spurious reporting of CSP
// violation, and thus the leakage of information to the remote site.
var matches = reScriptsrc.exec(csp);
if ( matches === null ) {
csp += "; script-src 'unsafe-eval' *";
headers.push({
'name': 'Content-Security-Policy',
'value': csp.replace(reReporturi, '')
});
return headers;
} }
// A `script-src' directive is already present. Extract it. // https://bugs.chromium.org/p/chromium/issues/detail?id=513860
var scriptsrc = matches[0]; // Bad Chromium bug: web pages can work around CSP directives by
// creating data:- or blob:-based URI. So if we must restrict using CSP,
// Is there at least one 'unsafe-inline' or 'nonce-' token in the // we have no choice but to also prevent the creation of nested browsing
// script-src? // contexts based on data:- or blob:-based URIs.
// If not we have no further processing to perform: inline scripts are if ( vAPI.chrome && (noInlineScript || noWebsocket) ) {
// already forbidden by the site. // https://w3c.github.io/webappsec-csp/#directive-frame-src
if ( reUnsafeinline.test(scriptsrc) === false ) { after = foilWithCSPDirective(
headers.push({ after,
'name': 'Content-Security-Policy', /frame-src[^;]*;?\s*/,
'value': csp 'frame-src http:',
}); /data:[^\s]*\s*|blob:[^\s]*\s*/g
return headers; );
} }
// There are tokens enabling inline script tags in the script-src var changed = after !== before;
// directive, so we have to strip them out. if ( changed ) {
// Strip out whole script-src directive, remove the offending tokens if ( i !== -1 ) {
// from it, then append the resulting script-src directive to the original headers.splice(i, 1);
// CSP header. }
// https://github.com/gorhill/uMatrix/issues/320 headers.push({ name: 'Content-Security-Policy', value: after });
// Since we are modifying an existing CSP header, we need to strip out
// 'report-uri' if it is present, to prevent spurious reporting of CSP
// violation, and thus the leakage of information to the remote site.
csp = csp.replace(reScriptsrc, '').trim();
// https://github.com/gorhill/uBlock/issues/1909
// Add missing `;` if needed.
if ( csp !== '' && csp.slice(-1) !== ';' ) {
csp += '; ';
} }
csp += scriptsrc.replace(reUnsafeinline, '').trim();
headers.push({ return changed;
'name': 'Content-Security-Policy',
'value': csp.replace(reReporturi, '')
});
return headers;
}; };
var reReporturi = /report-uri[^;]*;?\s*/; /******************************************************************************/
var reScriptsrc = /script-src[^;]*;?\s*/;
var reUnsafeinline = /'unsafe-inline'\s*|'nonce-[^']+'\s*/g; // Past issues to keep in mind:
// - https://github.com/gorhill/uMatrix/issues/129
// - https://github.com/gorhill/uMatrix/issues/320
// - https://github.com/gorhill/uBlock/issues/1909
var foilWithCSPDirective = function(csp, toExtract, toAdd, toRemove) {
// Set
if ( csp === '' ) {
return toAdd;
}
var matches = toExtract.exec(csp);
// Add
if ( matches === null ) {
if ( csp.slice(-1) !== ';' ) {
csp += ';';
}
csp += ' ' + toAdd;
return csp.replace(reReportDirective, '');
}
var directive = matches[0];
// No change
if ( toRemove.test(directive) === false ) {
return csp;
}
// Remove
csp = csp.replace(toExtract, '').trim();
if ( csp.slice(-1) !== ';' ) {
csp += ';';
}
directive = directive.replace(toRemove, '').trim();
// Check for empty directive after removal
matches = reEmptyDirective.exec(directive);
if ( matches ) {
directive = matches[1] + " 'none';";
}
csp += ' ' + directive;
return csp.replace(reReportDirective, '');
};
// https://w3c.github.io/webappsec-csp/#directives-reporting
var reReportDirective = /report-(?:to|uri)[^;]*;?\s*/;
var reEmptyDirective = /^([a-z-]+)\s*;/;
/******************************************************************************/ /******************************************************************************/