diff --git a/add-on/background_scripts/background.js b/add-on/background_scripts/background.js index f75a50f..cd38ec5 100644 --- a/add-on/background_scripts/background.js +++ b/add-on/background_scripts/background.js @@ -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( diff --git a/add-on/config/js/components/domain-rules.vue.js b/add-on/config/js/components/domain-rules.vue.js index 2d4ef41..5c9547e 100644 --- a/add-on/config/js/components/domain-rules.vue.js +++ b/add-on/config/js/components/domain-rules.vue.js @@ -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: ` diff --git a/add-on/config/js/components/logging-settings.vue.js b/add-on/config/js/components/logging-settings.vue.js index e1da6b4..ac7938d 100644 --- a/add-on/config/js/components/logging-settings.vue.js +++ b/add-on/config/js/components/logging-settings.vue.js @@ -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: ` diff --git a/add-on/config/js/components/web-api-standards.vue.js b/add-on/config/js/components/web-api-standards.vue.js index c72474a..f406871 100644 --- a/add-on/config/js/components/web-api-standards.vue.js +++ b/add-on/config/js/components/web-api-standards.vue.js @@ -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"], diff --git a/add-on/config/js/config.js b/add-on/config/js/config.js index a73dee6..b54bc5d 100644 --- a/add-on/config/js/config.js +++ b/add-on/config/js/config.js @@ -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); diff --git a/add-on/content_scripts/instrument.js b/add-on/content_scripts/instrument.js index 65b5696..223ae51 100644 --- a/add-on/content_scripts/instrument.js +++ b/add-on/content_scripts/instrument.js @@ -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); }()); diff --git a/add-on/lib/defaults.js b/add-on/lib/defaults.js index 36a4c17..8d4262f 100644 --- a/add-on/lib/defaults.js +++ b/add-on/lib/defaults.js @@ -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 diff --git a/add-on/lib/domainmatcher.js b/add-on/lib/domainmatcher.js index 3ede33b..f91694d 100644 --- a/add-on/lib/domainmatcher.js +++ b/add-on/lib/domainmatcher.js @@ -1,5 +1,3 @@ -/*jslint es6: true*/ -/*global window*/ (function () { "use strict"; const defaultKey = "(default)"; diff --git a/add-on/lib/httpheaders.js b/add-on/lib/httpheaders.js index 2043692..6ac068a 100644 --- a/add-on/lib/httpheaders.js +++ b/add-on/lib/httpheaders.js @@ -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 }; }()); \ No newline at end of file diff --git a/add-on/lib/init.js b/add-on/lib/init.js index 43e488a..81f8407 100644 --- a/add-on/lib/init.js +++ b/add-on/lib/init.js @@ -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 () { diff --git a/add-on/lib/pack.js b/add-on/lib/pack.js index 2e5b220..10328d8 100644 --- a/add-on/lib/pack.js +++ b/add-on/lib/pack.js @@ -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. // diff --git a/add-on/lib/proxyblock.js b/add-on/lib/proxyblock.js index 7edd05f..63a73b2 100644 --- a/add-on/lib/proxyblock.js +++ b/add-on/lib/proxyblock.js @@ -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 diff --git a/add-on/lib/storage.js b/add-on/lib/storage.js index 7dfd847..6d92c24 100644 --- a/add-on/lib/storage.js +++ b/add-on/lib/storage.js @@ -1,5 +1,3 @@ -/*jslint es6: true*/ -/*global window*/ (function () { "use strict"; diff --git a/add-on/manifest.json b/add-on/manifest.json index 78bc085..5c5cb84 100644 --- a/add-on/manifest.json +++ b/add-on/manifest.json @@ -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", diff --git a/package.json b/package.json index 91df0c7..868ce6a 100644 --- a/package.json +++ b/package.json @@ -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 (https://www.cs.uic.edu/~psnyder/)", "license": "GPL-3.0", diff --git a/test.config.example.js b/test.config.example.js index dc6a1f1..0412312 100644 --- a/test.config.example.js +++ b/test.config.example.js @@ -12,5 +12,9 @@ module.exports = { google: { username: "", password: "" + }, + facebook: { + username: "", + password: "" } }; \ No newline at end of file diff --git a/test/functional/block.js b/test/functional/block.js index bd239c4..664272c 100644 --- a/test/functional/block.js +++ b/test/functional/block.js @@ -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 = ` - - - Test Page - - -

Test Content

- - - `; - 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 () { diff --git a/test/functional/csp.js b/test/functional/csp.js new file mode 100644 index 0000000..5f27812 --- /dev/null +++ b/test/functional/csp.js @@ -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); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/functional/lib/server.js b/test/functional/lib/server.js new file mode 100644 index 0000000..ae84cb1 --- /dev/null +++ b/test/functional/lib/server.js @@ -0,0 +1,39 @@ +"use strict"; + +const http = require("http"); + +const testPort = 8989; +const testUrl = `http://localhost:${testPort}`; + +const staticResponse = ` + + + Test Page + + +

Test Content

+ + +`; + +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(); +}; \ No newline at end of file diff --git a/test/functional/logins.js b/test/functional/logins.js index 5c4f227..d16cd41 100644 --- a/test/functional/logins.js +++ b/test/functional/logins.js @@ -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();