summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhackademix2018-08-27 00:31:37 +0200
committerhackademix2018-08-27 18:55:00 +0200
commite2b63cf98204a45f4c55ba446689d20e524c188c (patch)
treefa9739889307b55777d4b48326bbad38136dabf0
parent6e80d3f130773fc9a9123c5c4c2e97d63e90fa2a (diff)
downloadnoscript-e2b63cf98204a45f4c55ba446689d20e524c188c.tar.gz
noscript-e2b63cf98204a45f4c55ba446689d20e524c188c.tar.xz
noscript-e2b63cf98204a45f4c55ba446689d20e524c188c.zip
Further CSP refactoring and removal of obsolete fallbacks.
-rw-r--r--src/bg/ChildPolicies.js72
-rw-r--r--src/bg/ReportingCSP.js39
-rw-r--r--src/bg/RequestGuard.js148
-rw-r--r--src/bg/main.js16
-rw-r--r--src/content/DocumentCSP.js7
-rw-r--r--src/content/PlaceHolder.js26
-rw-r--r--src/content/content.js218
-rw-r--r--src/content/dynamicNS.js21
-rw-r--r--src/content/embeddingDocument.js20
-rw-r--r--src/content/media.js4
-rw-r--r--src/content/staticNS.js99
-rw-r--r--src/content/webglHook.js4
-rw-r--r--src/lib/CSP.js1
-rw-r--r--src/lib/NetCSP.js2
-rw-r--r--src/manifest.json22
-rw-r--r--src/xss/sanitizeName.js2
16 files changed, 339 insertions, 362 deletions
diff --git a/src/bg/ChildPolicies.js b/src/bg/ChildPolicies.js
index 32abafe..91263fd 100644
--- a/src/bg/ChildPolicies.js
+++ b/src/bg/ChildPolicies.js
@@ -1,6 +1,7 @@
"use strict";
{
let marker = JSON.stringify(uuid());
+ let allUrls = ["<all_urls>"];
let Scripts = {
references: new Set(),
@@ -10,27 +11,52 @@
matchAboutBlank: true,
runAt: "document_start"
},
+ async init() {
+ let opts = Object.assign({}, this.opts);
+ opts.js = [{file: "/content/dynamicNS.js"}];
+ opts.matches = allUrls;
+ delete opts.excludedMatches;
+ this._stubScript = await browser.contentScripts.register(opts);
+
+ this.init = this.forget;
+ },
forget() {
for (let script of [...this.references]) {
script.unregister();
this.references.delete(script);
}
},
+ debug: false,
+ trace(code) {
+ return this.debug
+ ? `console.debug("Executing child policy", ${JSON.stringify(code)});${code}`
+ : code
+ ;
+ },
async register(code, matches, excludeMatches) {
debug("Registering child policy.", code, matches, excludeMatches);
if (!matches.length) return;
try {
- this.opts.js[0].code = code;
- this.opts.matches = matches;
+ let opts = Object.assign({}, this.opts);
+ opts.js[0].code = this.trace(code);
+ opts.matches = matches;
if (excludeMatches && excludeMatches.length) {
- this.opts.excludeMatches = excludeMatches;
- } else {
- delete this.opts.excludeMatches;
+ opts.excludeMatches = excludeMatches;
}
- this.references.add(await browser.contentScripts.register(this.opts));
+ this.references.add(await browser.contentScripts.register(opts));
} catch (e) {
error(e);
}
+ },
+
+ buildPerms(perms, finalizeSetup = false) {
+ if (typeof perms !== "string") {
+ perms = JSON.stringify(perms);
+ }
+ return finalizeSetup
+ ? `ns.setup(${perms}, ${marker});`
+ : `ns.config.CURRENT = ${perms};`
+ ;
}
};
@@ -69,12 +95,13 @@
error(e);
}
},
- async update(policy) {
- Scripts.forget();
+ async update(policy, debug) {
+ if (debug !== "undefined") Scripts.debug = debug;
+
+ await Scripts.init();
if (!policy.enforced) {
- await Scripts.register(`ns.setup(null, ${marker});`,
- ["<all_urls>"]);
+ await Scripts.register(`ns.setup(null, ${marker});`, allUrls);
return;
}
@@ -122,10 +149,27 @@
// register new content scripts
for (let [perms, keys] of [...permsMap]) {
- await Scripts.register(`ns.perms.CURRENT = ${perms};`, siteKeys2MatchPatterns(keys), excludeMap.get(perms));
+ await Scripts.register(Scripts.buildPerms(perms), siteKeys2MatchPatterns(keys), excludeMap.get(perms));
}
- await Scripts.register(`ns.setup(${JSON.stringify(serialized.DEFAULT)}, ${marker});`,
- ["<all_urls>"]);
+ await Scripts.register(Scripts.buildPerms(serialized.DEFAULT, true), allUrls);
+ },
+
+ getForDocument(policy, url, context = null) {
+ return {
+ CURRENT: policy.get(url, context).perms.dry(),
+ DEFAULT: policy.DEFAULT.dry(),
+ MARKER: marker
+ };
+ },
+
+ async updateFrame(tabId, frameId, perms, defaultPreset) {
+ let code = Scripts.buildPerms(perms) + Scripts.buildPerms(defaultPreset, true);
+ await browser.tabs.executeScript(tabId, {
+ code,
+ frameId,
+ matchAboutBlank: true,
+ runAt: "document_start"
+ });
}
- }
+ };
}
diff --git a/src/bg/ReportingCSP.js b/src/bg/ReportingCSP.js
index f8764e8..03926c2 100644
--- a/src/bg/ReportingCSP.js
+++ b/src/bg/ReportingCSP.js
@@ -1,6 +1,12 @@
"use strict";
-function ReportingCSP(reportURI, reportGroup) {
+function ReportingCSP(reportURI, reportGroup) {
+ const REPORT_TO = {
+ name: "Report-To",
+ value: JSON.stringify({ "url": reportURI,
+ "group": reportGroup,
+ "max-age": 10886400 }),
+ };
return Object.assign(
new CapsCSP(new NetCSP(
`report-uri ${reportURI};`,
@@ -9,11 +15,32 @@ function ReportingCSP(reportURI, reportGroup) {
{
reportURI,
reportGroup,
- reportToHeader: {
- name: "Report-To",
- value: JSON.stringify({ "url": reportURI,
- "group": reportGroup,
- "max-age": 10886400 }),
+ patchHeaders(responseHeaders, capabilities) {
+ let header = null;
+ let hasReportTo = false;
+ for (let h of responseHeaders) {
+ if (this.isMine(h)) {
+ header = h;
+ h.value = this.inject(h.value, "");
+ } else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
+ hasReportTo = true;
+ }
+ }
+
+ let blocker = capabilities && this.buildFromCapabilities(capabilities);
+ if (blocker) {
+ if (!hasReportTo) {
+ responseHeaders.push(REPORT_TO);
+ }
+ if (header) {
+ header.value = this.inject(header.value, blocker);
+ } else {
+ header = this.asHeader(blocker);
+ responseHeaders.push(header);
+ }
+ }
+
+ return header;
}
}
);
diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js
index cf2ff71..5dea994 100644
--- a/src/bg/RequestGuard.js
+++ b/src/bg/RequestGuard.js
@@ -2,12 +2,9 @@ var RequestGuard = (() => {
'use strict';
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
browser.browserAction.setTitle({title: VERSION_LABEL});
-
const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
const REPORT_GROUP = "NoScript-Endpoint";
-
let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
-
const policyTypesMap = {
main_frame: "",
sub_frame: "frame",
@@ -25,7 +22,6 @@ var RequestGuard = (() => {
};
const allTypes = Object.keys(policyTypesMap);
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
-
const TabStatus = {
map: new Map(),
types: ["script", "object", "media", "frame", "font"],
@@ -36,13 +32,11 @@ var RequestGuard = (() => {
noscriptFrames: {},
}
},
-
initTab(tabId, records = this.newRecords()) {
if (tabId < 0) return;
this.map.set(tabId, records);
return records;
},
-
_record(request, what, optValue) {
let {tabId, frameId, type, url, documentUrl} = request;
let policyType = policyTypesMap[type] || type;
@@ -54,7 +48,6 @@ var RequestGuard = (() => {
} else {
records = this.initTab(tabId);
}
-
if (what === "noscriptFrame" && type !== "object") {
let nsf = records.noscriptFrames;
nsf[frameId] = optValue;
@@ -76,7 +69,6 @@ var RequestGuard = (() => {
}
return records;
},
-
record(request, what, optValue) {
let {tabId} = request;
if (tabId < 0) return;
@@ -85,9 +77,7 @@ var RequestGuard = (() => {
this.updateTab(request.tabId);
}
},
-
_pendingTabs: new Set(),
-
updateTab(tabId) {
if (tabId < 0) return;
if (this._pendingTabs.size === 0) {
@@ -105,22 +95,18 @@ var RequestGuard = (() => {
let records = this.map.get(tabId) || this.initTab(tabId);
let {allowed, blocked, noscriptFrames} = records;
let topAllowed = !(noscriptFrames && noscriptFrames[0]);
-
let numAllowed = 0, numBlocked = 0, sum = 0;
let report = this.types.map(t => {
let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b;
numAllowed+= a, numBlocked += b, sum += s;
return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
}).filter(s => s).join("\n");
-
let enforced = ns.isEnforced(tabId);
-
let icon = topAllowed ?
(numBlocked ? "part"
: enforced ? "yes" : "global")
: (numAllowed ? "sub" : "no");
let showBadge = ns.local.showCountBadge && numBlocked > 0;
-
let browserAction = browser.browserAction;
browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}});
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
@@ -131,11 +117,9 @@ var RequestGuard = (() => {
: _("NotEnforced")}`
});
},
-
totalize(sum, value) {
return sum + value;
},
-
async probe(tabId) {
if (tabId === undefined) {
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
@@ -147,7 +131,6 @@ var RequestGuard = (() => {
}
}
},
-
recordAll(tabId, seen) {
if (seen) {
let records = TabStatus.map.get(tabId);
@@ -156,17 +139,21 @@ var RequestGuard = (() => {
records.blocked = {};
}
for (let thing of seen) {
- thing.request.tabId = tabId;
- TabStatus._record(thing.request, thing.allowed ? "allowed" : "blocked");
+ let {request, allowed} = thing;
+ request.tabId = tabId;
+ debug(`Recording`, request);
+ TabStatus._record(request, allowed ? "allowed" : "blocked");
+ if (request.key === "noscript-probe" && request.type === "main_frame" ) {
+ request.frameId = 0;
+ TabStatus._record(request, "noscriptFrame", !allowed);
+ }
}
this._updateTabNow(tabId);
}
},
-
async onActivatedTab(info) {
let {tabId} = info;
let seen = await ns.collectSeen(tabId);
-
TabStatus.recordAll(tabId, seen);
},
onRemovedTab(tabId) {
@@ -175,12 +162,9 @@ var RequestGuard = (() => {
}
browser.tabs.onActivated.addListener(TabStatus.onActivatedTab);
browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab);
-
if (!("setIcon" in browser.browserAction)) { // unsupported on Android
TabStatus._updateTabNow = TabStatus.updateTab = () => {};
}
-
-
let messageHandler = {
async pageshow(message, sender) {
TabStatus.recordAll(sender.tab.id, message.seen);
@@ -215,7 +199,6 @@ var RequestGuard = (() => {
if (!capabilities.has(policyType)) {
perms = new Permissions(new Set(capabilities), false);
perms.capabilities.add(policyType);
-
/* TODO: handle contextual permissions
if (documentUrl) {
let context = new URL(documentUrl).origin;
@@ -228,23 +211,8 @@ var RequestGuard = (() => {
}
return true;
},
-
- async queryDocStatus(message, sender) {
- let {frameId, tab} = sender;
- let {url} = message;
- let tabId = tab.id;
- let records = TabStatus.map.get(tabId);
- let noscriptFrames = records && records.noscriptFrames;
- let canScript = !(noscriptFrames && noscriptFrames[sender.frameId]);
- let shouldScript = !ns.isEnforced(tabId) || !url.startsWith("http") || ns.policy.can(url, "script");
- debug("Frame %s %s of %o, canScript: %s, shouldScript: %s", frameId, url, noscriptFrames, canScript, shouldScript);
- return {canScript, shouldScript};
- }
-
}
-
const Content = {
-
async reportTo(request, allowed, policyType) {
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
let pending = pendingRequests.get(requestId); // null if from a CSP report
@@ -276,7 +244,6 @@ var RequestGuard = (() => {
}
}
};
-
const pendingRequests = new Map();
function initPendingRequest(request) {
let {requestId, url} = request;
@@ -288,8 +255,6 @@ var RequestGuard = (() => {
});
return redirected;
}
-
-
const ABORT = {cancel: true}, ALLOW = {};
const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/;
const listeners = {
@@ -307,7 +272,6 @@ var RequestGuard = (() => {
// livemark request or similar browser-internal, always allow;
return ALLOW;
}
-
if (/^(?:data|blob):/.test(url)) {
request._dataUrl = url;
request.url = url = documentUrl;
@@ -316,7 +280,6 @@ var RequestGuard = (() => {
!ns.isEnforced(request.tabId) ||
policy.can(url, policyType, originUrl);
Content.reportTo(request, allowed, policyType);
-
if (!allowed) {
debug(`Blocking ${policyType}`, request);
TabStatus.record(request, "blocked");
@@ -326,13 +289,10 @@ var RequestGuard = (() => {
} catch (e) {
error(e);
}
-
return ALLOW;
},
-
async onHeadersReceived(request) {
// called for main_frame, sub_frame and object
-
// check for duplicate calls
let pending = pendingRequests.get(request.requestId);
if (pending) {
@@ -347,67 +307,28 @@ var RequestGuard = (() => {
pending = pendingRequests.get(request.requestId);
}
pending.headersProcessed = true;
-
let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request;
-
let isMainFrame = type === "main_frame";
-
try {
- let header;
- let content = {};
- const REPORT_TO = csp.reportToHeader;
- let hasReportTo = false;
- for (let h of responseHeaders) {
- if (csp.isMine(h)) {
- header = h;
- h.value = csp.inject(h.value, "");
- } else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) {
- content[RegExp.$1.toLowerCase()] = h.value;
- } else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
- hasReportTo = true;
- }
- }
-
+ let capabilities;
if (ns.isEnforced(tabId)) {
let policy = ns.policy;
let perms = policy.get(url, documentUrl).perms;
-
if (policy.autoAllowTop && isMainFrame && perms === policy.DEFAULT) {
policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin);
await ChildPolicies.update(policy);
}
-
- let blockHttp = !content.disposition &&
- (!content.type || /^\s*(?:video|audio|application)\//.test(content.type));
- if (blockHttp) {
- debug(`Suspicious content type "%s" in request %o with capabilities %o`,
- content.type, request, capabilities);
- }
-
- let blocker = csp.buildFromCapabilities(perms.capabilities, blockHttp);
- if (blocker) {
- if (!hasReportTo) {
- responseHeaders.push(csp.reportToHeader);
- }
-
- if (header) {
- pending.cspHeader = header;
- header.value = csp.inject(header.value, blocker);
- } else {
- header = csp.asHeader(blocker);
- responseHeaders.push(header);
- }
-
- debug(`CSP blocker on %s:`, url, blocker, header.value);
- }
-
- if (isMainFrame && !TabStatus.map.has(tabId)) {
- debug("No TabStatus data yet for noscriptFrame", tabId);
- TabStatus.record(request, "noscriptFrame", true);
- }
+ capabilities = perms.capabilities;
}
-
+ if (isMainFrame && !TabStatus.map.has(tabId)) {
+ debug("No TabStatus data yet for noscriptFrame", tabId);
+ TabStatus.record(request, "noscriptFrame",
+ capabilities && !capabilities.has("script"));
+ }
+ let header = csp.patchHeaders(responseHeaders, capabilities);
if (header) {
+ pending.cspHeader = header;
+ debug(`CSP blocker on %s:`, url, header.value);
return {responseHeaders};
}
} catch (e) {
@@ -415,7 +336,6 @@ var RequestGuard = (() => {
}
return ALLOW;
},
-
onResponseStarted(request) {
debug("onResponseStarted", request);
let {requestId, url, tabId, frameId, type} = request;
@@ -430,25 +350,15 @@ var RequestGuard = (() => {
let pending = pendingRequests.get(requestId);
if (pending) {
pending.scriptBlocked = scriptBlocked;
- if (!(pending.headersProcessed &&
+ if (!(pending.headersProcessed &&
(scriptBlocked || !ns.isEnforced(tabId) || ns.policy.can(url, "script", request.documentURL))
)) {
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId,
- pending.headersProcessed ? "has been overridden on": "could not process",
+ pending.headersProcessed ? "has been overridden on": "could not process",
request);
-
- if (tabId !== -1 && type !== "object") {
- debug("[WARNING] Reloading %s frame %s of tab %s.", url, frameId, tabId);
- browser.tabs.executeScript(tabId, {
- runAt: "document_start",
- code: "window.location.reload(false)",
- frameId
- });
- }
}
}
},
-
onCompleted(request) {
let {requestId} = request;
if (pendingRequests.has(requestId)) {
@@ -463,12 +373,10 @@ var RequestGuard = (() => {
}
}
},
-
onErrorOccurred(request) {
pendingRequests.delete(request.requestId);
}
};
-
function fakeRequestFromCSP(report, request) {
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
if (type === "frame") type = "sub_frame";
@@ -479,7 +387,6 @@ var RequestGuard = (() => {
type,
});
}
-
async function onViolationReport(request) {
try {
let decoder = new TextDecoder("UTF-8");
@@ -501,28 +408,21 @@ var RequestGuard = (() => {
}
return ABORT;
}
-
const RequestGuard = {
async start() {
Messages.addHandler(messageHandler);
-
let wr = browser.webRequest;
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
-
let allUrls = ["<all_urls>"];
let docTypes = ["main_frame", "sub_frame", "object"];
-
let filterDocs = {urls: allUrls, types: docTypes};
let filterAll = {urls: allUrls, types: allTypes};
-
listen("onBeforeRequest", filterAll, ["blocking"]);
-
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
-
(listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => {
let {requestId, responseHeaders} = request;
let pending = pendingRequests.get(request.requestId);
- if (pending && pending.headersProcessed) {
+ if (pending && pending.headersProcessed) {
let {cspHeader} = pending;
if (cspHeader) {
debug("Safety net: injecting again %o in %o", cspHeader, request);
@@ -541,18 +441,13 @@ var RequestGuard = (() => {
}
return null;
}, filterDocs, ["blocking", "responseHeaders"])).install();
-
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
listen("onCompleted", filterAll);
listen("onErrorOccurred", filterAll);
-
-
wr.onBeforeRequest.addListener(onViolationReport,
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
-
TabStatus.probe();
},
-
stop() {
let wr = browser.webRequest;
for (let [name, listener] of Object.entries(listeners)) {
@@ -566,6 +461,5 @@ var RequestGuard = (() => {
Messages.removeHandler(messageHandler);
}
};
-
return RequestGuard;
})();
diff --git a/src/bg/main.js b/src/bg/main.js
index 47d28a3..fa2831c 100644
--- a/src/bg/main.js
+++ b/src/bg/main.js
@@ -23,10 +23,13 @@
}
async function init() {
+ await include("/bg/defaults.js");
+ await ns.defaults;
+
let policyData = (await Storage.get("sync", "policy")).policy;
if (policyData && policyData.DEFAULT) {
ns.policy = new Policy(policyData);
- await ChildPolicies.update(policyData);
+ await ChildPolicies.update(policyData, ns.local.debug);
} else {
await include("/legacy/Legacy.js");
ns.policy = await Legacy.createOrMigratePolicy();
@@ -34,8 +37,7 @@
}
- await include("/bg/defaults.js");
- await ns.defaults;
+
await include("/bg/RequestGuard.js");
await RequestGuard.start();
await XSS.start(); // we must start it anyway to initialize sub-objects
@@ -135,7 +137,11 @@
async importSettings({data}) {
return await Settings.import(data);
},
-
+
+ async fetchChildPolicy({url, contextUrl}) {
+ return ChildPolicies.getForDocument(ns.policy, url, contextUrl);
+ },
+
async openStandalonePopup() {
let win = await browser.windows.getLastFocused();
let [tab] = (await browser.tabs.query({
@@ -203,7 +209,7 @@
async savePolicy() {
if (this.policy) {
- await ChildPolicies.update(this.policy);
+ await ChildPolicies.update(this.policy, this.local.debug);
await Storage.set("sync", {
policy: this.policy.dry()
});
diff --git a/src/content/DocumentCSP.js b/src/content/DocumentCSP.js
index 228b2a2..371e547 100644
--- a/src/content/DocumentCSP.js
+++ b/src/content/DocumentCSP.js
@@ -6,9 +6,9 @@ class DocumentCSP {
this.builder = new CapsCSP();
}
- apply(capabilities) {
+ apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) {
let csp = this.builder;
- let blocker = csp.buildFromCapabilities(capabilities);
+ let blocker = csp.buildFromCapabilities(capabilities, embedding);
if (!blocker) return;
let document = this.document;
@@ -19,8 +19,11 @@ class DocumentCSP {
let parent = document.head || document.documentElement;
try {
parent.insertBefore(meta, parent.firstChild);
+ debug(`Failsafe <meta> CSP inserted in the DOM: "%s"`, header.value);
+ if (capabilities.has("script")) meta.remove();
} catch (e) {
error(e, "Error inserting CSP %s in the DOM", header && header.value);
}
}
+
}
diff --git a/src/content/PlaceHolder.js b/src/content/PlaceHolder.js
index ec2ab5b..09f6767 100644
--- a/src/content/PlaceHolder.js
+++ b/src/content/PlaceHolder.js
@@ -1,6 +1,17 @@
var PlaceHolder = (() => {
const HANDLERS = new Map();
-
+
+ let checkStyle = async () => {
+ checkStyle = () => {};
+ if (!ns.embeddingDocument) return;
+ let replacement = document.querySelector("a.__NoScript_PlaceHolder__");
+ if (!replacement) return;
+ if (window.getComputedStyle(replacement, null).opacity !== "0.8") {
+ document.head.appendChild(createHTMLElement("style")).textContent = await
+ (await fetch(browser.extension.getURL("/content/content.css"))).text();
+ }
+ }
+
class Handler {
constructor(type, selector) {
this.type = type;
@@ -9,6 +20,7 @@ var PlaceHolder = (() => {
HANDLERS.set(type, this);
}
filter(element, request) {
+ if (request.embeddingDocument) return true;
let url = request.initialUrl || request.url;
return "data" in element ? element.data === url : element.src === url;
}
@@ -77,10 +89,14 @@ var PlaceHolder = (() => {
.filter(element => this.handler.filter(element, request))
.forEach(element => this.replace(element));
};
- if (this.replacements.size) PlaceHolder.listen();
+ if (this.replacements.size) {
+ PlaceHolder.listen();
+ checkStyle();
+ }
}
replace(element) {
+ if (!element.parentElement) return;
let {
url
} = this.request;
@@ -108,10 +124,10 @@ var PlaceHolder = (() => {
replacement._placeHolderObj = this;
replacement._placeHolderElement = element;
- this.replacements.add(replacement);
+
- if (element.parentNode) element.parentNode.replaceChild(replacement, element);
- else document.body.appendChild(replacement);
+ element.parentNode.replaceChild(replacement, element);
+ this.replacements.add(replacement);
}
async enable(replacement) {
diff --git a/src/content/content.js b/src/content/content.js
index a5d996d..c7fc045 100644
--- a/src/content/content.js
+++ b/src/content/content.js
@@ -1,112 +1,12 @@
'use strict';
+// debug = () => {}; // REL_ONLY
- // debug = () => {}; // REL_ONLY
-{
- let listenersMap = new Map();
- let backlog = new Set();
- var ns = {
- on(eventName, listener) {
- let listeners = listenersMap.get(eventName);
- if (!listeners) listenersMap.set(eventName, listeners = new Set());
- listeners.add(listener);
- if (backlog.has(eventName)) this.fire(eventName, listener);
- },
- detach(eventName, listener) {
- let listeners = listenersMap.get(eventName);
- if (listeners) listeners.delete(listener);
- },
- fire(eventName, listener = null) {
- if (listener) {
- listener({type:eventName, source: this});
- return;
- }
- let listeners = listenersMap.get(eventName);
- if (listeners) {
- for (let l of listeners) {
- this.fire(eventName, l);
- }
- }
- backlog.add(eventName);
- },
- setup(DEFAULT, MARKER) {
- this.perms.DEFAULT = DEFAULT;
- if(!this.perms.CURRENT) this.perms.CURRENT = DEFAULT;
-
- // ugly hack: since now we use registerContentScript instead of the
- // filterRequest dynamic script injection hack, we use top.name
- // to store per-tab information. We don't want web content to
- // mess with it, though, so we wrap it around auto-hiding accessors
- this.perms.MARKER = MARKER;
- let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`);
- if (eraseTabInfoRx.test(top.name)) {
- let _name = top.name;
- let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`);
- if (top === window) { // wrap to hide
- Reflect.defineProperty(top.wrappedJSObject, "name", {
- get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject),
- set: exportFunction(value => {
- let preamble = top.name.match(tabInfoRx);
- top.name = `${preamble && preamble[0] || ""}${value}`;
- return value;
- }, top.wrappedJSObject)
- });
- }
- let tabInfoMatch = _name.match(tabInfoRx);
- if (tabInfoMatch) try {
- this.perms.tabInfo = JSON.parse(tabInfoMatch[1]);
- } catch (e) {
- error(e);
- }
- }
-
- if (!this.perms.DEFAULT || this.perms.tabInfo.unrestricted) {
- this.allows = () => true;
- this.capabilities = Object.assign(
- new Set(["script"]), { has() { return true; } });
- } else {
- let perms = this.perms.CURRENT || this.perms.DEFAULT;
- this.capabilities = new Set(perms.capabilities);
- new DocumentCSP(document).apply(this.capabilities);
- }
- ns.fire("perms");
- },
- perms: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" },
-
- allows(cap) {
- return this.capabilities && this.capabilities.has(cap);
- },
-
- getWindowName() {
- return top !== window || !this.perms.MARKER ? window.name
- : window.name.split(this.perms.MARKER + ",").pop();
- }
- }
-}
-
-var canScript = true, shouldScript = false;
-
-let now = () => performance.now() + performance.timeOrigin;
+var _ = browser.i18n.getMessage;
function createHTMLElement(name) {
return document.createElementNS("http://www.w3.org/1999/xhtml", name);
}
-function probe() {
- try {
- debug("Probing execution...");
- let s = document.createElement("script");
- s.textContent = ";";
- document.documentElement.appendChild(s);
- s.remove();
- } catch(e) {
- debug(e);
- }
-}
-
-var _ = browser.i18n.getMessage;
-
-var embeddingDocument = false;
-
var seen = {
_map: new Map(),
_list: null,
@@ -128,9 +28,8 @@ Messages.addHandler({
seen.record(event);
}
if (ownFrame) {
- init();
if (!allowed && PlaceHolder.canReplace(policyType)) {
- request.embeddingDocument = embeddingDocument;
+ request.embeddingDocument = ns.embeddingDocument;
PlaceHolder.create(policyType, request);
}
}
@@ -142,87 +41,38 @@ Messages.addHandler({
}
});
-if (document.readyState !== "complete") {
- let pageshown = e => {
- removeEventListener("pageshow", pageshown);
- init();
- };
- addEventListener("pageshow", pageshown);
-} else {
- init(true);
-}
-let notifyPage = async () => {
+
+debug(`Loading NoScript in document %s, scripting=%s, readyState %s`,
+ document.URL, ns.canScript, document.readyState);
+
+var notifyPage = async () => {
debug("Page %s shown, %s", document.URL, document.readyState);
if (document.readyState === "complete") {
try {
- await Messages.send("pageshow", {seen: seen.list, canScript});
+ if (!("canScript" in ns)) {
+ let childPolicy = await Messages.send("fetchChildPolicy", {url: document.URL, contextUrl: top.location.href});
+ ns.config.CURRENT = childPolicy.CURRENT;
+ ns.setup(childPolicy.DEFAULT, childPolicy.MARKER);
+ return;
+ }
+
+ await Messages.send("pageshow", {seen: seen.list, canScript: ns.canScript});
return true;
} catch (e) {
debug(e);
+ if (/Receiving end does not exist/.test(e.message)) {
+ window.setTimeout(notifyPage, 2000);
+ }
}
}
return false;
}
-var queryingStatus = false;
-
-function reload(noCache = false) {
- init = () => {};
- location.reload(noCache);
-}
+notifyPage();
-async function init(oldPage = false) {
- if (queryingStatus) return;
- if (!document.URL.startsWith("http")) {
- return;
- }
- queryingStatus = true;
+window.addEventListener("pageshow", notifyPage);
- debug(`init() called in document %s, contentType %s readyState %s, frameElement %o`,
- document.URL, document.contentType, document.readyState, window.frameElement && frameElement.data);
-
- try {
- ({canScript, shouldScript} = await Messages.send("queryDocStatus", {url: document.URL}));
- debug(`document %s, canScript=%s, shouldScript=%s, readyState %s`, document.URL, canScript, shouldScript, document.readyState);
- if (canScript) {
- if (oldPage) {
- probe();
- return;
- }
- if (!shouldScript &&
- (document.readyState !== "complete" ||
- now() - performance.timing.domContentLoadedEventStart < 5000)) {
- // Something wrong: scripts can run, permissions say they shouldn't.
- // Was webRequest bypassed by caching/session restore/service workers?
- window.stop();
- let noCache = !!navigator.serviceWorker.controller;
- if (noCache) {
- for (let r of await navigator.serviceWorker.getRegistrations()) {
- await r.unregister();
- }
- }
- debug("Reloading %s (%s)", document.URL, noCache ? "no cache" : "cached");
- reload(noCache);
- return;
- }
- }
- init = () => {};
- } catch (e) {
- debug("Error querying docStatus", e);
- if (!oldPage &&
- /Receiving end does not exist/.test(e.message)) {
- // probably startup and bg page not ready yet, hence no CSP: reload!
- debug("Reloading", document.URL);
- reload();
- } else {
- setTimeout(() => init(oldPage), 100);
- }
- return;
- } finally {
- queryingStatus = false;
- }
-
- if (!canScript) onScriptDisabled();
+ns.on("capabilities", () => {
seen.record({
request: {
key: "noscript-probe",
@@ -230,21 +80,13 @@ async function init(oldPage = false) {
documentUrl: document.URL,
type: window === window.top ? "main_frame" : "script",
},
- allowed: canScript
- }
- );
-
- debug(`Loading NoScript in document %s, scripting=%s, readyState %s`,
- document.URL, canScript, document.readyState);
-
- if (/application|video|audio/.test(document.contentType)) {
- debug("Embedding document detected");
- embeddingDocument = true;
- window.addEventListener("pageshow", e => {
- debug("Active content still in document %s: %o", document.url, document.querySelectorAll("embed,object,video,audio"));
- }, true);
- // document.write("<plaintext>");
+ allowed: ns.canScript
+ });
+
+ if (!ns.canScript) {
+ if (document.readyState !== "loading") onScriptDisabled();
+ window.addEventListener("DOMContentLoaded", onScriptDisabled);
}
+
notifyPage();
- addEventListener("pageshow", notifyPage);
-}
+});
diff --git a/src/content/dynamicNS.js b/src/content/dynamicNS.js
new file mode 100644
index 0000000..cdd7a0e
--- /dev/null
+++ b/src/content/dynamicNS.js
@@ -0,0 +1,21 @@
+'use strict';
+
+// ensure the order which manifest scripts and dynamically registered scripts
+// are executed in doesn't matter for initialization, by using a stub.
+
+if (!this.ns) {
+ let deferredSetup = null;
+ let nsStub = this.ns = {
+ config: {},
+ setup(DEFAULT, MARKER) {
+ deferredSetup = [DEFAULT, MARKER];
+ },
+ merge: ns => {
+ ns.config = Object.assign(ns.config, nsStub.config);
+ this.ns = ns;
+ if (deferredSetup) {
+ ns.setup(...deferredSetup);
+ }
+ }
+ }
+}
diff --git a/src/content/embeddingDocument.js b/src/content/embeddingDocument.js
new file mode 100644
index 0000000..75b0db0
--- /dev/null
+++ b/src/content/embeddingDocument.js
@@ -0,0 +1,20 @@
+if (ns.embeddingDocument) {
+ ns.on("capabilities", () => {
+ for (let policyType of ["object", "media"]) {
+ if (!ns.allows(policyType)) {
+ let request = {
+ id: `noscript-${policyType}-doc`,
+ type: policyType,
+ url: document.URL,
+ documentUrl: document.URL,
+ embeddingDocument: true,
+ };
+ let ph = PlaceHolder.create(policyType, request);
+ if (ph.replacements.size > 0) {
+ debug(`Created placeholder for ${policyType} at ${document.URL}`);
+ seen.record({policyType, request, allowed: false});
+ }
+ }
+ }
+ });
+}
diff --git a/src/content/media.js b/src/content/media.js
index 910fd27..770a43f 100644
--- a/src/content/media.js
+++ b/src/content/media.js
@@ -1,5 +1,5 @@
-ns.on("perms", event => {
- debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY
+ns.on("capabilities", event => {
+ debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY
let mediaBlocker = !ns.allows("media");
let unpatched = new Map();
function patch(obj, methodName, replacement) {
diff --git a/src/content/staticNS.js b/src/content/staticNS.js
new file mode 100644
index 0000000..817351a
--- /dev/null
+++ b/src/content/staticNS.js
@@ -0,0 +1,99 @@
+'use strict';
+{
+ let listenersMap = new Map();
+ let backlog = new Set();
+
+ let ns = {
+ debug: true, // DEV_ONLY
+ get embeddingDocument() {
+ delete this.embeddingDocument;
+ return this.embeddingDocument = CSP.isEmbedType(document.contentType);
+ },
+ on(eventName, listener) {
+ let listeners = listenersMap.get(eventName);
+ if (!listeners) listenersMap.set(eventName, listeners = new Set());
+ listeners.add(listener);
+ if (backlog.has(eventName)) this.fire(eventName, listener);
+ },
+ detach(eventName, listener) {
+ let listeners = listenersMap.get(eventName);
+ if (listeners) listeners.delete(listener);
+ },
+ fire(eventName, listener = null) {
+ if (listener) {
+ listener({type:eventName, source: this});
+ return;
+ }
+ let listeners = listenersMap.get(eventName);
+ if (listeners) {
+ for (let l of listeners) {
+ this.fire(eventName, l);
+ }
+ }
+ backlog.add(eventName);
+ },
+
+ setup(DEFAULT, MARKER) {
+ this.config.DEFAULT = DEFAULT;
+ if(!this.config.CURRENT) this.config.CURRENT = DEFAULT;
+
+ // ugly hack: since now we use registerContentScript instead of the
+ // filterRequest dynamic script injection hack, we use top.name
+ // to store per-tab information. We don't want web content to
+ // mess with it, though, so we wrap it around auto-hiding accessors
+ this.config.MARKER = MARKER;
+ let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`);
+ if (eraseTabInfoRx.test(top.name)) {
+ let _name = top.name;
+ let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`);
+ if (top === window) { // wrap to hide
+ Reflect.defineProperty(top.wrappedJSObject, "name", {
+ get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject),
+ set: exportFunction(value => {
+ let preamble = top.name.match(tabInfoRx);
+ top.name = `${preamble && preamble[0] || ""}${value}`;
+ return value;
+ }, top.wrappedJSObject)
+ });
+ }
+ let tabInfoMatch = _name.match(tabInfoRx);
+ if (tabInfoMatch) try {
+ this.config.tabInfo = JSON.parse(tabInfoMatch[1]);
+ } catch (e) {
+ error(e);
+ }
+ }
+
+ if (!this.config.DEFAULT || this.config.tabInfo.unrestricted) {
+ this.allows = () => true;
+ this.capabilities = Object.assign(
+ new Set(["script"]), { has() { return true; } });
+ } else {
+ let perms = this.config.CURRENT;
+ this.capabilities = new Set(perms.capabilities);
+ new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument);
+ }
+
+ this.canScript = this.allows("script");
+ this.fire("capabilities");
+ },
+ config: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" },
+
+ allows(cap) {
+ return this.capabilities && this.capabilities.has(cap);
+ },
+
+ getWindowName() {
+ let marker = this.config.MARKER;
+ return (top === window && marker) ?
+ window.name.split(`${marker},`).pop()
+ : window.name;
+ }
+ };
+
+ if (this.ns) {
+ this.ns.merge(ns);
+ } else {
+ this.ns = ns;
+ }
+}
diff --git a/src/content/webglHook.js b/src/content/webglHook.js
index efafcd5..4475585 100644
--- a/src/content/webglHook.js
+++ b/src/content/webglHook.js
@@ -1,5 +1,5 @@
-ns.on("perms", event => {
- debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY
+ns.on("capabilities", event => {
+ debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY
if (ns.allows("webgl")) return;
let proto = HTMLCanvasElement.prototype;
let getContext = proto.getContext;
diff --git a/src/lib/CSP.js b/src/lib/CSP.js
index 8550f09..79590bc 100644
--- a/src/lib/CSP.js
+++ b/src/lib/CSP.js
@@ -19,4 +19,5 @@ class CSP {
}
}
+CSP.isEmbedType = type => /\b(?:application|video|audio)\b/.test(type);
CSP.headerName = "content-security-policy";
diff --git a/src/lib/NetCSP.js b/src/lib/NetCSP.js
index cb79a80..90ef8ad 100644
--- a/src/lib/NetCSP.js
+++ b/src/lib/NetCSP.js
@@ -27,4 +27,6 @@ class NetCSP extends CSP {
return `${this.start}${super.build(...directives)}${this.end}`;
}
+ cleanup(headers) {
+ }
}
diff --git a/src/manifest.json b/src/manifest.json
index 143a776..7a78bef 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -60,6 +60,14 @@
"content_scripts": [
{
+ "matches": ["<all_urls>"],
+ "match_about_blank": true,
+ "all_frames": true,
+ "css": [
+ "/content/content.css"
+ ]
+ },
+ {
"run_at": "document_start",
"matches": ["<all_urls>"],
"match_about_blank": true,
@@ -71,20 +79,14 @@
"common/CapsCSP.js",
"content/DocumentCSP.js",
"content/onScriptDisabled.js",
+ "content/staticNS.js",
"content/content.js",
- "content/webglHook.js",
"content/PlaceHolder.js",
+ "content/embeddingDocument.js",
+ "content/webglHook.js",
"content/media.js"
]
- },
- {
- "matches": ["<all_urls>"],
- "match_about_blank": true,
- "all_frames": true,
- "css": [
- "/content/content.css"
- ]
- }
+ }
],
"options_ui": {
diff --git a/src/xss/sanitizeName.js b/src/xss/sanitizeName.js
index 4f36cbf..2a8acb6 100644
--- a/src/xss/sanitizeName.js
+++ b/src/xss/sanitizeName.js
@@ -1,4 +1,4 @@
-ns.on("perms", event => {
+ns.on("capabilities", event => {
if (ns.allows("script")) {
let name = ns.getWindowName();
if (/[<"'\`(=:]/.test(name)) {