mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-10 09:07:54 +01:00
fix #1936: ability to foil WebSocket using a CSP directive
This commit is contained in:
parent
48dcca0250
commit
8586aee848
1 changed files with 140 additions and 130 deletions
|
@ -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*;/;
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue