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 () {
"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(

View file

@ -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: `

View file

@ -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: `

View file

@ -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"],

View file

@ -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);

View file

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

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
* overwritten, either by the extension user, or by a subscribed policy

View file

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

View file

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

View file

@ -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 () {

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
// in a compact, base64 representation.
//

View file

@ -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

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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
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 () {
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();