address newly introduced CSP issues by not inserting the hash allowed rule for {default,script}-src if there is a 'unsafe-line' rule in place, fixes #11
This commit is contained in:
parent
1732d8fa96
commit
0bd91f6f3f
20 changed files with 230 additions and 130 deletions
|
@ -1,5 +1,3 @@
|
|||
/*jslint es6: true*/
|
||||
/*global window*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
|
@ -156,8 +154,7 @@
|
|||
// injected proxy code to the list of scripts that are allowed to
|
||||
// run in the page.
|
||||
const cspDynamicPolicyHeaders = details.responseHeaders
|
||||
.filter(httpHeadersLib.isHeaderCSP)
|
||||
.filter(httpHeadersLib.isCSPHeaderSettingScriptSrc);
|
||||
.filter(httpHeadersLib.isHeaderCSPScriptSrcWithOutUnsafeInline);
|
||||
|
||||
if (cspDynamicPolicyHeaders.length === 1) {
|
||||
const [ignore, scriptHash] = proxyBlockLib.generateScriptPayload(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*jslint es6: true, this: true*/
|
||||
/*global window, Vue*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const Vue = window.Vue;
|
||||
|
||||
Vue.component("domain-rules", {
|
||||
props: ["domainNames", "selectedDomain"],
|
||||
template: `
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*jslint es6: true, this: true*/
|
||||
/*global window, Vue*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const Vue = window.Vue;
|
||||
|
||||
Vue.component("logging-settings", {
|
||||
props: ["shouldLog"],
|
||||
template: `
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/*jslint es6: true, this: true*/
|
||||
/*global window, Vue*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const standardsDefaults = window.WEB_API_MANAGER.defaults;
|
||||
const Vue = window.Vue;
|
||||
|
||||
Vue.component("web-api-standards", {
|
||||
props: ["standards", "selectedStandards", "selectedDomain"],
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/*jslint es6: true*/
|
||||
/*global window, browser, chrome, Vue*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
|
@ -8,6 +6,7 @@
|
|||
const standards = window.WEB_API_MANAGER.standards;
|
||||
const {storageLib, stateLib} = window.WEB_API_MANAGER;
|
||||
const defaultDomain = "(default)";
|
||||
const Vue = window.Vue;
|
||||
|
||||
const state = stateLib.generateStateObject(defaultDomain, standards);
|
||||
|
||||
|
|
|
@ -13,28 +13,47 @@
|
|||
const script = doc.createElement('script');
|
||||
const rootElm = doc.head || doc.documentElement;
|
||||
|
||||
const cookieValue = cookies2.get(standardsCookieName);
|
||||
// First see if we can read the standards to block out of the cookie
|
||||
// sent from the extension. If not, then try to read it out of local
|
||||
// storage (which may be needed if there are multiple requests to the same
|
||||
// domain that interleave so that the cookie is deleted by one request
|
||||
// before it can be read out by the other).
|
||||
let domainPref;
|
||||
|
||||
if (!cookieValue) {
|
||||
try {
|
||||
domainPref = cookies2.get(standardsCookieName);
|
||||
cookies2.remove(standardsCookieName, {path: window.document.location.pathname});
|
||||
} catch (e) {
|
||||
// This can happen if an iframe tries to read the cookie created from
|
||||
// a parent request without the allow-same-origin attribute.
|
||||
}
|
||||
|
||||
// If we couldn't read the domain preferences out of the cookie, then
|
||||
// see if we can read it out of localStorage.
|
||||
if (!domainPref) {
|
||||
if (window.localStorage) {
|
||||
domainPref = window.localStorage[standardsCookieName];
|
||||
}
|
||||
} else {
|
||||
// Otherwise, if we did read things out of the cookie, then store
|
||||
// it in local storage, so that other requests to the same origin
|
||||
// can read the blocking settings.
|
||||
window.localStorage[standardsCookieName] = domainPref;
|
||||
}
|
||||
|
||||
if (!domainPref) {
|
||||
window.console.log(`Unable to find the Web API Manager settings for ${doc.location.href}`);
|
||||
return;
|
||||
}
|
||||
|
||||
cookies2.remove(standardsCookieName, {path: window.document.location.pathname});
|
||||
const [standardsToBlock, shouldLog] = cookieEncodingLib.fromCookieValue(domainPref);
|
||||
|
||||
if (!cookieValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [standardsToBlock, shouldLog] = cookieEncodingLib.fromCookieValue(cookieValue);
|
||||
|
||||
const [scriptToInject, scriptHash] = proxyBlockLib.generateScriptPayload(
|
||||
const [scriptToInject, ignore] = proxyBlockLib.generateScriptPayload(
|
||||
standards,
|
||||
standardsToBlock,
|
||||
shouldLog
|
||||
);
|
||||
|
||||
script.appendChild(doc.createTextNode(scriptToInject));
|
||||
script.integrity = "sha256-" + scriptHash;
|
||||
rootElm.appendChild(script);
|
||||
}());
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/*jslint es6: true, browser: true*/
|
||||
/*global window*/
|
||||
/**
|
||||
* This file defines default blocking rules for domains that haven't been
|
||||
* overwritten, either by the extension user, or by a subscribed policy
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/*jslint es6: true*/
|
||||
/*global window*/
|
||||
(function () {
|
||||
"use strict";
|
||||
const defaultKey = "(default)";
|
||||
|
|
|
@ -1,89 +1,64 @@
|
|||
/*jslint es6: true*/
|
||||
/*global window*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Returns a boolean description of whether the given header
|
||||
* (in the structure defined by the WebExtension WebRequest API)
|
||||
* is describing a Set-Cookie instruction.
|
||||
* matches the following critera:
|
||||
* 1. Is a content-security-policy instruction
|
||||
* 2. Includes either a script-src or default-src rule, and
|
||||
* 3. That rule _does not_ include an 'unsafe-inline' instruction.
|
||||
*
|
||||
* This function is used to determine whether we need to inject a hash
|
||||
* of the injected proxyblocking code into the pages CSP policy, to white
|
||||
* list our script.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/HttpHeaders
|
||||
* @see https://w3c.github.io/webappsec-csp/
|
||||
*
|
||||
* @param object header
|
||||
* An object describing a HTTP header
|
||||
*
|
||||
* @return boolean
|
||||
* true if the given object represents a Set-Cookie instruction, and false
|
||||
* in all other cases.
|
||||
* true if the given object depicts a CSP policy with the above stated
|
||||
* properties, and false in all other cases.
|
||||
*/
|
||||
const isSetCookie = function (header) {
|
||||
const isHeaderCSPScriptSrcWithOutUnsafeInline = function (header) {
|
||||
|
||||
return (
|
||||
header &&
|
||||
header.name &&
|
||||
header.name.toLowerCase().indexOf("set-cookie") !== -1
|
||||
);
|
||||
};
|
||||
if (!header ||
|
||||
!header.name ||
|
||||
header.name.toLowerCase().indexOf("content-security-policy") === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isNotHTTPOnlySetCookie = function (header) {
|
||||
const cspInstruction = header.value;
|
||||
let relevantRule;
|
||||
|
||||
return (
|
||||
header &&
|
||||
header.value &&
|
||||
header.value.toLowerCase().indexOf("httponly") === -1
|
||||
);
|
||||
};
|
||||
if (cspInstruction.indexOf("script-src ") !== -1) {
|
||||
relevantRule = "script-src";
|
||||
} else if (cspInstruction.indexOf("default-src ") !== -1) {
|
||||
relevantRule = "default-src";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean description of whether the given header
|
||||
* (in the structure defined by the WebExtension WebRequest API)
|
||||
* is describing a Content-Security-Policy for a site.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/HttpHeaders
|
||||
*
|
||||
* @param object header
|
||||
* An object describing a HTTP header
|
||||
*
|
||||
* @return boolean
|
||||
* true if the given object represents a HTTP CSP header, and false
|
||||
* in all other cases.
|
||||
*/
|
||||
const isHeaderCSP = function (header) {
|
||||
const scriptSrcInstructionPattern = new RegExp(relevantRule + " .*?(?:;|$)", "i");
|
||||
const match = scriptSrcInstructionPattern.exec(cspInstruction);
|
||||
|
||||
return (
|
||||
header &&
|
||||
header.name &&
|
||||
header.name.toLowerCase().indexOf("content-security-policy") !== -1
|
||||
);
|
||||
};
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean description of whether the given header
|
||||
* (in the structure defined by the WebExtension WebRequest API)
|
||||
* is describing a strict dynamic Content-Security-Policy for a site.
|
||||
*
|
||||
* @see https://w3c.github.io/webappsec-csp/#strict-dynamic-usage
|
||||
*
|
||||
* @param object header
|
||||
* An object describing a HTTP header
|
||||
*
|
||||
* @return boolean
|
||||
* true if the given object is a CSP header that defines a
|
||||
* "strict-dynamic" policy, and false in all other cases.
|
||||
*/
|
||||
const isCSPHeaderSettingScriptSrc = function (header) {
|
||||
|
||||
return (
|
||||
header &&
|
||||
header.value &&
|
||||
header.value.indexOf("script-src") !== -1
|
||||
);
|
||||
return match[0].indexOf("'unsafe-inline'") === -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a new CSP instruction, with source with the given hash
|
||||
* whitelisted.
|
||||
*
|
||||
* If the CSP instruction has a "script-src" rule, then the hash-value
|
||||
* will be inserted there. Otherwise, it will be inserted in the
|
||||
* default-src section.
|
||||
*
|
||||
* @see https://w3c.github.io/webappsec-csp/#strict-dynamic-usage
|
||||
* @see https://w3c.github.io/webappsec-csp/#grammardef-hash-source
|
||||
|
@ -96,28 +71,35 @@
|
|||
*
|
||||
* @return string|false
|
||||
* Returns false if the CSP instruction looks malformed (ie we
|
||||
* couldn't find a "script-src" tag), otherwise, a new valud
|
||||
* CSP instruction with the given hash allowed.
|
||||
* couldn't find either a "script-src" or "default-src" section),
|
||||
* otherwise, a new value CSP instruction with the given hash allowed.
|
||||
*/
|
||||
const createCSPInstructionWithHashAllowed = function (cspInstruction, scriptHash) {
|
||||
|
||||
const indexOfScriptSrc = cspInstruction.indexOf("script-src ");
|
||||
if (indexOfScriptSrc === -1) {
|
||||
const indexOfDefaultSrc = cspInstruction.indexOf("default-src ");
|
||||
|
||||
let ruleToModify, indexOfRuleStart;
|
||||
if (indexOfScriptSrc !== -1) {
|
||||
ruleToModify = "script-src";
|
||||
indexOfRuleStart = indexOfScriptSrc;
|
||||
} else if (indexOfDefaultSrc !== -1) {
|
||||
ruleToModify = "default-src";
|
||||
indexOfRuleStart = indexOfDefaultSrc;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const lengthOfRule = ruleToModify.length;
|
||||
|
||||
const preSrcScript = cspInstruction.substring(0, indexOfScriptSrc);
|
||||
const postScriptSrc = cspInstruction.substring(indexOfScriptSrc + 11);
|
||||
const newInstruction = preSrcScript + "script-src '" + scriptHash + "' " + postScriptSrc;
|
||||
const preSrcRule = cspInstruction.substring(0, indexOfRuleStart);
|
||||
const postSrcRule = cspInstruction.substring(indexOfRuleStart + lengthOfRule);
|
||||
const newInstruction = preSrcRule + ruleToModify + " '" + scriptHash + "' " + postSrcRule;
|
||||
|
||||
return newInstruction;
|
||||
};
|
||||
|
||||
window.WEB_API_MANAGER.httpHeadersLib = {
|
||||
isSetCookie,
|
||||
isNotHTTPOnlySetCookie,
|
||||
isHeaderCSP,
|
||||
isCSPHeaderSettingScriptSrc,
|
||||
isHeaderCSPScriptSrcWithOutUnsafeInline,
|
||||
createCSPInstructionWithHashAllowed
|
||||
};
|
||||
}());
|
|
@ -1,4 +1,3 @@
|
|||
/*global window*/
|
||||
// Initial content script for the Web API manager extension, that creates
|
||||
// the "namespace" we'll use for all the content scripts in the extension.
|
||||
(function () {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/*jslint es6: true, for: true, bitwise: true*/
|
||||
/*global window*/
|
||||
// Functions to represent a selection of options out of a bigger set
|
||||
// in a compact, base64 representation.
|
||||
//
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/*jslint es6: true, browser: true*/
|
||||
/*global window, sjcl*/
|
||||
/*globals sjcl*/
|
||||
// This module generates JavaScript code for instrumenting the DOM
|
||||
// to prevent pages from accessing Web API standards. This code
|
||||
// is generated programatically so that both the background and content
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/*jslint es6: true*/
|
||||
/*global window*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "WebAPI Manager",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"description": "Improves browser security and privacy by controlling page access to the Web API.",
|
||||
"icons": {
|
||||
"48": "images/uic-48.png",
|
||||
|
@ -46,8 +46,8 @@
|
|||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"lib/vendor/sjcl.js",
|
||||
"lib/vendor/URI.js",
|
||||
"lib/vendor/sjcl.js",
|
||||
"lib/init.js",
|
||||
"lib/standards.js",
|
||||
"lib/pack.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "web-api-manager",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"description": "Tools to generate Web API managing browser extensions for Firefox and Chrome.",
|
||||
"author": "Peter Snyder <psnyde2@uic.edu> (https://www.cs.uic.edu/~psnyder/)",
|
||||
"license": "GPL-3.0",
|
||||
|
|
|
@ -12,5 +12,9 @@ module.exports = {
|
|||
google: {
|
||||
username: "",
|
||||
password: ""
|
||||
},
|
||||
facebook: {
|
||||
username: "",
|
||||
password: ""
|
||||
}
|
||||
};
|
|
@ -1,42 +1,28 @@
|
|||
"use strict";
|
||||
|
||||
const http = require("http");
|
||||
const utils = require("./lib/utils");
|
||||
const injected = require("./lib/injected");
|
||||
const testServer = require("./lib/server");
|
||||
|
||||
describe("Basic", function () {
|
||||
|
||||
const testPort = 8989;
|
||||
const testUrl = `http://localhost:${testPort}`;
|
||||
const svgTestScript = injected.testSVGTestScript();
|
||||
|
||||
let httpServer;
|
||||
let testUrl;
|
||||
|
||||
this.timeout = function () {
|
||||
return 10000;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
httpServer = http.createServer(function (req, res) {
|
||||
const staticResponse = `<!DOCTYPE "html">
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test Content</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
res.writeHead(200, {"Content-Type": "text/html"});
|
||||
res.write(staticResponse);
|
||||
res.end();
|
||||
});
|
||||
httpServer.listen(8989);
|
||||
const [server, url] = testServer.start();
|
||||
testUrl = url;
|
||||
httpServer = server;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
httpServer.close();
|
||||
testServer.stop(httpServer);
|
||||
});
|
||||
|
||||
describe("blocking", function () {
|
||||
|
|
43
test/functional/csp.js
Normal file
43
test/functional/csp.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
"use strict";
|
||||
|
||||
const utils = require("./lib/utils");
|
||||
const injected = require("./lib/injected");
|
||||
const testServer = require("./lib/server");
|
||||
|
||||
describe("Content-Security-Protocol tests", function () {
|
||||
|
||||
describe("script-src", function () {
|
||||
|
||||
this.timeout = () => 20000;
|
||||
|
||||
it("default-src and script-src (from Pitchfork.com)", function (done) {
|
||||
|
||||
const [server, testUrl] = testServer.start(function (headers) {
|
||||
// Add the CSP header to every request
|
||||
headers['Content-Security-Protocol'] = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; child-src https: data: blob:; connect-src https: data: blob:; font-src https: data:; img-src https: data: blob:; media-src https: data: blob:; object-src https:; script-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://capture.condenastdigital.com/csp/pitchfork;";
|
||||
});
|
||||
|
||||
const svgTestScript = injected.testSVGTestScript();
|
||||
const standardsToBlock = utils.constants.svgBlockRule;
|
||||
let driverReference;
|
||||
|
||||
utils.promiseGetDriver()
|
||||
.then(function (driver) {
|
||||
driverReference = driver;
|
||||
return utils.promiseSetBlockingRules(driver, standardsToBlock);
|
||||
})
|
||||
.then(() => driverReference.get(testUrl))
|
||||
.then(() => driverReference.executeAsyncScript(svgTestScript))
|
||||
.then(function () {
|
||||
driverReference.quit();
|
||||
testServer.stop(server);
|
||||
done();
|
||||
})
|
||||
.catch(function (e) {
|
||||
driverReference.quit();
|
||||
testServer.stop(server);
|
||||
done(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
39
test/functional/lib/server.js
Normal file
39
test/functional/lib/server.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
const http = require("http");
|
||||
|
||||
const testPort = 8989;
|
||||
const testUrl = `http://localhost:${testPort}`;
|
||||
|
||||
const staticResponse = `<!DOCTYPE "html">
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test Content</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
module.exports.start = function (callback) {
|
||||
|
||||
const httpServer = http.createServer(function (req, res) {
|
||||
|
||||
let headers = {"Content-Type": "text/html; charset=utf-8"};
|
||||
|
||||
if (callback !== undefined) {
|
||||
callback(headers);
|
||||
}
|
||||
|
||||
res.writeHead(200, headers);
|
||||
res.write(staticResponse);
|
||||
res.end();
|
||||
});
|
||||
httpServer.listen(8989);
|
||||
return [httpServer, testUrl];
|
||||
};
|
||||
|
||||
module.exports.stop = function (server) {
|
||||
server.close();
|
||||
};
|
|
@ -92,6 +92,48 @@ describe("Logging into popular sites", function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Facebook", function () {
|
||||
this.timeout = function () {
|
||||
return 20000;
|
||||
};
|
||||
|
||||
it("Log in", function (done) {
|
||||
|
||||
const formValues = [
|
||||
["email", testParams.facebook.username],
|
||||
["pass", testParams.facebook.password]
|
||||
];
|
||||
|
||||
let driverReference;
|
||||
|
||||
utils.promiseGetDriver()
|
||||
.then(function (driver) {
|
||||
driverReference = driver;
|
||||
return driverReference.get("https://www.facebook.com/");
|
||||
})
|
||||
.then(function () {
|
||||
return driverReference.wait(until.elementsLocated(
|
||||
by.name("email")
|
||||
), 5000);
|
||||
})
|
||||
.then(() => utils.promiseSetFormAndSubmit(driverReference, formValues))
|
||||
.then(function () {
|
||||
return driverReference.wait(until.elementLocated(
|
||||
by.css("div[data-click='profile_icon']")
|
||||
), 10000);
|
||||
})
|
||||
.then(function () {
|
||||
driverReference.quit();
|
||||
done();
|
||||
})
|
||||
.catch(function (e) {
|
||||
driverReference.quit();
|
||||
console.log(e);
|
||||
done(new Error("Was not able to log in"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("YouTube", function () {
|
||||
|
||||
this.timeout = function () {
|
||||
|
@ -110,7 +152,7 @@ describe("Logging into popular sites", function () {
|
|||
.then(function () {
|
||||
return driverReference.wait(until.elementsLocated(
|
||||
by.css("#buttons ytd-button-renderer a")
|
||||
), 2000);
|
||||
), 5000);
|
||||
})
|
||||
.then(anchors => anchors[anchors.length - 1].click())
|
||||
.then(() => utils.pause(2000))
|
||||
|
@ -132,7 +174,7 @@ describe("Logging into popular sites", function () {
|
|||
.then(function () {
|
||||
return driverReference.wait(until.elementLocated(
|
||||
by.css("ytd-app")
|
||||
), 40000);
|
||||
), 10000);
|
||||
})
|
||||
.then(function () {
|
||||
driverReference.quit();
|
||||
|
|
Loading…
Reference in a new issue