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:
Peter Snyder 2017-10-23 00:17:23 -05:00
parent 1732d8fa96
commit 0bd91f6f3f
20 changed files with 230 additions and 130 deletions

View file

@ -1,5 +1,3 @@
/*jslint es6: true*/
/*global window*/
(function () { (function () {
"use strict"; "use strict";
@ -156,8 +154,7 @@
// injected proxy code to the list of scripts that are allowed to // injected proxy code to the list of scripts that are allowed to
// run in the page. // run in the page.
const cspDynamicPolicyHeaders = details.responseHeaders const cspDynamicPolicyHeaders = details.responseHeaders
.filter(httpHeadersLib.isHeaderCSP) .filter(httpHeadersLib.isHeaderCSPScriptSrcWithOutUnsafeInline);
.filter(httpHeadersLib.isCSPHeaderSettingScriptSrc);
if (cspDynamicPolicyHeaders.length === 1) { if (cspDynamicPolicyHeaders.length === 1) {
const [ignore, scriptHash] = proxyBlockLib.generateScriptPayload( const [ignore, scriptHash] = proxyBlockLib.generateScriptPayload(

View file

@ -1,8 +1,8 @@
/*jslint es6: true, this: true*/
/*global window, Vue*/
(function () { (function () {
"use strict"; "use strict";
const Vue = window.Vue;
Vue.component("domain-rules", { Vue.component("domain-rules", {
props: ["domainNames", "selectedDomain"], props: ["domainNames", "selectedDomain"],
template: ` template: `

View file

@ -1,8 +1,8 @@
/*jslint es6: true, this: true*/
/*global window, Vue*/
(function () { (function () {
"use strict"; "use strict";
const Vue = window.Vue;
Vue.component("logging-settings", { Vue.component("logging-settings", {
props: ["shouldLog"], props: ["shouldLog"],
template: ` template: `

View file

@ -1,9 +1,8 @@
/*jslint es6: true, this: true*/
/*global window, Vue*/
(function () { (function () {
"use strict"; "use strict";
const standardsDefaults = window.WEB_API_MANAGER.defaults; const standardsDefaults = window.WEB_API_MANAGER.defaults;
const Vue = window.Vue;
Vue.component("web-api-standards", { Vue.component("web-api-standards", {
props: ["standards", "selectedStandards", "selectedDomain"], props: ["standards", "selectedStandards", "selectedDomain"],

View file

@ -1,5 +1,3 @@
/*jslint es6: true*/
/*global window, browser, chrome, Vue*/
(function () { (function () {
"use strict"; "use strict";
@ -8,6 +6,7 @@
const standards = window.WEB_API_MANAGER.standards; const standards = window.WEB_API_MANAGER.standards;
const {storageLib, stateLib} = window.WEB_API_MANAGER; const {storageLib, stateLib} = window.WEB_API_MANAGER;
const defaultDomain = "(default)"; const defaultDomain = "(default)";
const Vue = window.Vue;
const state = stateLib.generateStateObject(defaultDomain, standards); const state = stateLib.generateStateObject(defaultDomain, standards);

View file

@ -13,28 +13,47 @@
const script = doc.createElement('script'); const script = doc.createElement('script');
const rootElm = doc.head || doc.documentElement; 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}`); window.console.log(`Unable to find the Web API Manager settings for ${doc.location.href}`);
return; return;
} }
cookies2.remove(standardsCookieName, {path: window.document.location.pathname}); const [standardsToBlock, shouldLog] = cookieEncodingLib.fromCookieValue(domainPref);
if (!cookieValue) { const [scriptToInject, ignore] = proxyBlockLib.generateScriptPayload(
return;
}
const [standardsToBlock, shouldLog] = cookieEncodingLib.fromCookieValue(cookieValue);
const [scriptToInject, scriptHash] = proxyBlockLib.generateScriptPayload(
standards, standards,
standardsToBlock, standardsToBlock,
shouldLog shouldLog
); );
script.appendChild(doc.createTextNode(scriptToInject)); script.appendChild(doc.createTextNode(scriptToInject));
script.integrity = "sha256-" + scriptHash;
rootElm.appendChild(script); rootElm.appendChild(script);
}()); }());

View file

@ -1,5 +1,3 @@
/*jslint es6: true, browser: true*/
/*global window*/
/** /**
* This file defines default blocking rules for domains that haven't been * This file defines default blocking rules for domains that haven't been
* overwritten, either by the extension user, or by a subscribed policy * overwritten, either by the extension user, or by a subscribed policy

View file

@ -1,5 +1,3 @@
/*jslint es6: true*/
/*global window*/
(function () { (function () {
"use strict"; "use strict";
const defaultKey = "(default)"; const defaultKey = "(default)";

View file

@ -1,89 +1,64 @@
/*jslint es6: true*/
/*global window*/
(function () { (function () {
"use strict"; "use strict";
/** /**
* Returns a boolean description of whether the given header * Returns a boolean description of whether the given header
* (in the structure defined by the WebExtension WebRequest API) * (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://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/HttpHeaders
* @see https://w3c.github.io/webappsec-csp/
* *
* @param object header * @param object header
* An object describing a HTTP header * An object describing a HTTP header
* *
* @return boolean * @return boolean
* true if the given object represents a Set-Cookie instruction, and false * true if the given object depicts a CSP policy with the above stated
* in all other cases. * properties, and false in all other cases.
*/ */
const isSetCookie = function (header) { const isHeaderCSPScriptSrcWithOutUnsafeInline = function (header) {
return ( if (!header ||
header && !header.name ||
header.name && header.name.toLowerCase().indexOf("content-security-policy") === -1) {
header.name.toLowerCase().indexOf("set-cookie") !== -1 return false;
); }
};
const isNotHTTPOnlySetCookie = function (header) { const cspInstruction = header.value;
let relevantRule;
return ( if (cspInstruction.indexOf("script-src ") !== -1) {
header && relevantRule = "script-src";
header.value && } else if (cspInstruction.indexOf("default-src ") !== -1) {
header.value.toLowerCase().indexOf("httponly") === -1 relevantRule = "default-src";
); } else {
}; return false;
}
/** const scriptSrcInstructionPattern = new RegExp(relevantRule + " .*?(?:;|$)", "i");
* Returns a boolean description of whether the given header const match = scriptSrcInstructionPattern.exec(cspInstruction);
* (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) {
return ( if (!match) {
header && return false;
header.name && }
header.name.toLowerCase().indexOf("content-security-policy") !== -1
);
};
/** return match[0].indexOf("'unsafe-inline'") === -1;
* 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
);
}; };
/** /**
* Returns a new CSP instruction, with source with the given hash * Returns a new CSP instruction, with source with the given hash
* whitelisted. * 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/#strict-dynamic-usage
* @see https://w3c.github.io/webappsec-csp/#grammardef-hash-source * @see https://w3c.github.io/webappsec-csp/#grammardef-hash-source
@ -96,28 +71,35 @@
* *
* @return string|false * @return string|false
* Returns false if the CSP instruction looks malformed (ie we * Returns false if the CSP instruction looks malformed (ie we
* couldn't find a "script-src" tag), otherwise, a new valud * couldn't find either a "script-src" or "default-src" section),
* CSP instruction with the given hash allowed. * otherwise, a new value CSP instruction with the given hash allowed.
*/ */
const createCSPInstructionWithHashAllowed = function (cspInstruction, scriptHash) { const createCSPInstructionWithHashAllowed = function (cspInstruction, scriptHash) {
const indexOfScriptSrc = cspInstruction.indexOf("script-src "); 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; return false;
} }
const lengthOfRule = ruleToModify.length;
const preSrcScript = cspInstruction.substring(0, indexOfScriptSrc); const preSrcRule = cspInstruction.substring(0, indexOfRuleStart);
const postScriptSrc = cspInstruction.substring(indexOfScriptSrc + 11); const postSrcRule = cspInstruction.substring(indexOfRuleStart + lengthOfRule);
const newInstruction = preSrcScript + "script-src '" + scriptHash + "' " + postScriptSrc; const newInstruction = preSrcRule + ruleToModify + " '" + scriptHash + "' " + postSrcRule;
return newInstruction; return newInstruction;
}; };
window.WEB_API_MANAGER.httpHeadersLib = { window.WEB_API_MANAGER.httpHeadersLib = {
isSetCookie, isHeaderCSPScriptSrcWithOutUnsafeInline,
isNotHTTPOnlySetCookie,
isHeaderCSP,
isCSPHeaderSettingScriptSrc,
createCSPInstructionWithHashAllowed createCSPInstructionWithHashAllowed
}; };
}()); }());

View file

@ -1,4 +1,3 @@
/*global window*/
// Initial content script for the Web API manager extension, that creates // Initial content script for the Web API manager extension, that creates
// the "namespace" we'll use for all the content scripts in the extension. // the "namespace" we'll use for all the content scripts in the extension.
(function () { (function () {

View file

@ -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 // Functions to represent a selection of options out of a bigger set
// in a compact, base64 representation. // in a compact, base64 representation.
// //

View file

@ -1,5 +1,4 @@
/*jslint es6: true, browser: true*/ /*globals sjcl*/
/*global window, sjcl*/
// This module generates JavaScript code for instrumenting the DOM // This module generates JavaScript code for instrumenting the DOM
// to prevent pages from accessing Web API standards. This code // to prevent pages from accessing Web API standards. This code
// is generated programatically so that both the background and content // is generated programatically so that both the background and content

View file

@ -1,5 +1,3 @@
/*jslint es6: true*/
/*global window*/
(function () { (function () {
"use strict"; "use strict";

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "WebAPI Manager", "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.", "description": "Improves browser security and privacy by controlling page access to the Web API.",
"icons": { "icons": {
"48": "images/uic-48.png", "48": "images/uic-48.png",
@ -46,8 +46,8 @@
], ],
"background": { "background": {
"scripts": [ "scripts": [
"lib/vendor/sjcl.js",
"lib/vendor/URI.js", "lib/vendor/URI.js",
"lib/vendor/sjcl.js",
"lib/init.js", "lib/init.js",
"lib/standards.js", "lib/standards.js",
"lib/pack.js", "lib/pack.js",

View file

@ -1,6 +1,6 @@
{ {
"name": "web-api-manager", "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.", "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/)", "author": "Peter Snyder <psnyde2@uic.edu> (https://www.cs.uic.edu/~psnyder/)",
"license": "GPL-3.0", "license": "GPL-3.0",

View file

@ -12,5 +12,9 @@ module.exports = {
google: { google: {
username: "", username: "",
password: "" password: ""
},
facebook: {
username: "",
password: ""
} }
}; };

View file

@ -1,42 +1,28 @@
"use strict"; "use strict";
const http = require("http");
const utils = require("./lib/utils"); const utils = require("./lib/utils");
const injected = require("./lib/injected"); const injected = require("./lib/injected");
const testServer = require("./lib/server");
describe("Basic", function () { describe("Basic", function () {
const testPort = 8989;
const testUrl = `http://localhost:${testPort}`;
const svgTestScript = injected.testSVGTestScript(); const svgTestScript = injected.testSVGTestScript();
let httpServer; let httpServer;
let testUrl;
this.timeout = function () { this.timeout = function () {
return 10000; return 10000;
}; };
beforeEach(function () { beforeEach(function () {
httpServer = http.createServer(function (req, res) { const [server, url] = testServer.start();
const staticResponse = `<!DOCTYPE "html"> testUrl = url;
<html> httpServer = server;
<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);
}); });
afterEach(function () { afterEach(function () {
httpServer.close(); testServer.stop(httpServer);
}); });
describe("blocking", function () { describe("blocking", function () {

43
test/functional/csp.js Normal file
View 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);
});
});
});
});

View 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();
};

View file

@ -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 () { describe("YouTube", function () {
this.timeout = function () { this.timeout = function () {
@ -110,7 +152,7 @@ describe("Logging into popular sites", function () {
.then(function () { .then(function () {
return driverReference.wait(until.elementsLocated( return driverReference.wait(until.elementsLocated(
by.css("#buttons ytd-button-renderer a") by.css("#buttons ytd-button-renderer a")
), 2000); ), 5000);
}) })
.then(anchors => anchors[anchors.length - 1].click()) .then(anchors => anchors[anchors.length - 1].click())
.then(() => utils.pause(2000)) .then(() => utils.pause(2000))
@ -132,7 +174,7 @@ describe("Logging into popular sites", function () {
.then(function () { .then(function () {
return driverReference.wait(until.elementLocated( return driverReference.wait(until.elementLocated(
by.css("ytd-app") by.css("ytd-app")
), 40000); ), 10000);
}) })
.then(function () { .then(function () {
driverReference.quit(); driverReference.quit();