Better file: protocol support.

This commit is contained in:
hackademix 2018-09-03 19:20:39 +02:00
parent 16cdbbe1cb
commit 81ac052e1d
9 changed files with 146 additions and 81 deletions

View File

@ -2,7 +2,7 @@
{
let marker = JSON.stringify(uuid());
let allUrls = ["<all_urls>"];
let Scripts = {
references: new Set(),
opts: {
@ -17,7 +17,7 @@
opts.matches = allUrls;
delete opts.excludedMatches;
this._stubScript = await browser.contentScripts.register(opts);
this.init = this.forget;
},
forget() {
@ -29,7 +29,7 @@
debug: false,
trace(code) {
return this.debug
? `console.debug("Executing child policy", ${JSON.stringify(code)});${code}`
? `console.debug("Executing child policy on %s", document.URL, ${JSON.stringify(code)});${code}`
: code
;
},
@ -38,7 +38,7 @@
if (!matches.length) return;
try {
let opts = Object.assign({}, this.opts);
opts.js[0].code = this.trace(code);
opts.js[0].code = this.trace(code);
opts.matches = matches;
if (excludeMatches && excludeMatches.length) {
opts.excludeMatches = excludeMatches;
@ -48,38 +48,51 @@
error(e);
}
},
buildPerms(perms, finalizeSetup = false) {
if (typeof perms !== "string") {
perms = JSON.stringify(perms);
}
return finalizeSetup
? `ns.setup(${perms}, ${marker});`
? `ns.setup(${perms}, ${marker});`
: `ns.config.CURRENT = ${perms};`
;
}
};
let flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
let protocolRx = /^(https?):/i;
let pathRx = /[^:/]\//;
let protocolRx = /^(\w+):/i;
let pathRx = /(?:[^:/]\/|:\/{3})$/;
let portRx = /:\d+(?=\/|$)/;
let validMatchPatternRx = /^(?:https?|\*):\/\/(?:\*\.)?(?:[\w\u0100-\uf000][\w\u0100-\uf000.-]*)?[\w\u0100-\uf000]\/(\*|[^*]*)$/;
let validMatchPatternRx = /^(?:\*|(?:http|ws|ftp)s?|file):\/\/(?:\*\.)?(?:[\w\u0100-\uf000][\w\u0100-\uf000.-]*)?\/(\*|[^*]*)$/;
let siteKey2MatchPattern = site => {
let hasProtocol = site.match(protocolRx);
let protocol = hasProtocol ? ''
: Sites.isSecureDomainKey(site) ? "https://" : "*://";
let hostname = Sites.toggleSecureDomainKey(site, false)
.replace(portRx, '');
if (!hasProtocol) hostname = `*.${hostname}`;
let path = pathRx.test(hostname) ? "" : "/*";
let mp = `${protocol}${hostname}${path}`;
return validMatchPatternRx.test(mp) && (path ? mp : [mp, `${mp}?*`, `${mp}#*`]);
let mp = site;
if (hasProtocol) {
try {
let url = new URL(site);
url.port = "";
url.search = "";
url.hash = "";
mp = url.href;
} catch (e) {
return false;
}
} else {
let protocol = Sites.isSecureDomainKey(site) ? "https://" : "*://";
let hostname = Sites.toggleSecureDomainKey(site, false)
.replace(portRx, '');
mp = `${protocol}*.${hostname}`;
if (!hostname.includes("/")) mp += "/";
}
return validMatchPatternRx.test(mp) && (
mp.endsWith("/") ? `${mp}*` : [mp, `${mp}?*`, `${mp}#*`]);
};
let siteKeys2MatchPatterns = keys => keys && flatten(keys.map(siteKey2MatchPattern)).filter(p => !!p) || [];
let siteKeys2MatchPatterns = keys => keys && flatten(keys.map(siteKey2MatchPattern)).filter(p => !!p) || [];
var ChildPolicies = {
async storeTabInfo(tabId, info) {
@ -90,21 +103,21 @@
allFrames: false,
matchAboutBlank: true,
runAt: "document_start",
});
});
} catch (e) {
error(e);
}
},
async update(policy, debug) {
if (debug !== "undefined") Scripts.debug = debug;
await Scripts.init();
if (!policy.enforced) {
await Scripts.register(`ns.setup(null, ${marker});`, allUrls);
return;
}
let serialized = policy.dry ? policy.dry(true) : policy;
let permsMap = new Map();
let trusted = JSON.stringify(serialized.TRUSTED);
@ -120,7 +133,7 @@
if (!(newKeys && newKeys.length)) continue;
let keys = permsMap.get(perms);
if (keys) {
newKeys = keys.concat(newKeys);
newKeys = keys.concat(newKeys);
}
permsMap.set(perms, newKeys);
}
@ -134,11 +147,11 @@
permsMap.set(permsKey, [key]);
}
}
// compute exclusions
let permsMapEntries = [...permsMap];
let excludeMap = new Map();
for (let [perms, keys] of permsMapEntries) {
excludeMap.set(perms, siteKeys2MatchPatterns(flatten(
permsMapEntries.filter(([other]) => other !== perms)
@ -146,14 +159,14 @@
.filter(k => k && k.includes("/") && keys.some(by => Sites.isImplied(k, by)))
));
}
// register new content scripts
for (let [perms, keys] of [...permsMap]) {
await Scripts.register(Scripts.buildPerms(perms), siteKeys2MatchPatterns(keys), excludeMap.get(perms));
}
await Scripts.register(Scripts.buildPerms(serialized.DEFAULT, true), allUrls);
},
getForDocument(policy, url, context = null) {
return {
CURRENT: policy.get(url, context).perms.dry(),
@ -161,7 +174,7 @@
MARKER: marker
};
},
async updateFrame(tabId, frameId, perms, defaultPreset) {
let code = Scripts.buildPerms(perms) + Scripts.buildPerms(defaultPreset, true);
await browser.tabs.executeScript(tabId, {
@ -169,7 +182,7 @@
frameId,
matchAboutBlank: true,
runAt: "document_start"
});
});
}
};
}

View File

@ -177,14 +177,12 @@ var RequestGuard = (() => {
let {siteKey} = Sites.parse(url);
let options;
if (siteKey === origin) {
TAG += `@${siteKey}`;
} else {
options = [
{label: _("allowLocal", siteKey), checked: true},
{label: _("allowLocal", origin)}
];
origin = new URL(url).protocol;
}
// let parsedDoc = Sites.parse(documentUrl);
options = [
{label: _("allowLocal", siteKey), checked: true},
{label: _("allowLocal", origin)}
];
let t = u => `${TAG}@${u}`;
let ret = await Prompts.prompt({
title: _("BlockedObjects"),

View File

@ -141,8 +141,9 @@
return await Settings.import(data);
},
async fetchChildPolicy({url, contextUrl}) {
return ChildPolicies.getForDocument(ns.policy, url, contextUrl);
async fetchChildPolicy({url, contextUrl}, sender) {
return ChildPolicies.getForDocument(ns.policy,
url || sender.url, contextUrl || sender.tab.url);
},
async openStandalonePopup() {

View File

@ -5,9 +5,10 @@ var {Permissions, Policy, Sites} = (() => {
const SECURE_DOMAIN_RX = new RegExp(`^${SECURE_DOMAIN_PREFIX}`);
const DOMAIN_RX = new RegExp(`(?:^\\w+://|${SECURE_DOMAIN_PREFIX})?([^/]*)`, "i");
const SKIP_RX = /^(?:(?:about|chrome|resource|moz-.*):|\[System)/;
const VALID_SITE_RX = /^(?:(?:(?:(?:http|ftp|ws)s?|file):)(?:(?:\/\/)[\w\u0100-\uf000][\w\u0100-\uf000.-]*[\w\u0100-\uf000](?:$|\/))?|[\w\u0100-\uf000][\w\u0100-\uf000.-]*[\w\u0100-\uf000]$)/;
let rxQuote = s => s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
class Sites extends Map {
static secureDomainKey(domain) {
return domain.includes(":") ? domain : `${SECURE_DOMAIN_PREFIX}${domain}`;
@ -20,14 +21,14 @@ var {Permissions, Policy, Sites} = (() => {
}
static isValid(site) {
return /^(?:https?:(?:\/\/)?)?([\w\u0100-\uf000][\w\u0100-\uf000.-]*)?[\w\u0100-\uf000](?::\d+)?$/.test(site);
return VALID_SITE_RX.test(site);
}
static originImplies(originKey, site) {
return originKey === site || site.startsWith(`${originKey}/`);
}
static domainImplies(domainKey, site, protocol ="https?") {
if (Sites.isSecureDomainKey(domainKey)) {
protocol = "https";
@ -42,13 +43,13 @@ var {Permissions, Policy, Sites} = (() => {
return false;
}
}
static isImplied(site, byKey) {
return byKey.includes("://")
return byKey.includes("://")
? Sites.originImplies(byKey, site)
: Sites.domainImplies(byKey, site);
}
static parse(site) {
let url, siteKey = "";
if (site instanceof URL) {
@ -63,7 +64,11 @@ var {Permissions, Policy, Sites} = (() => {
if (url) {
let path = url.pathname;
siteKey = url.origin;
if (path !== '/') siteKey += path;
if (siteKey === "null") {
siteKey = site;
} else if (path !== '/') {
siteKey += path;
}
}
return {url, siteKey};
}
@ -71,14 +76,16 @@ var {Permissions, Policy, Sites} = (() => {
static optimalKey(site) {
let {url, siteKey} = Sites.parse(site);
if (url && url.protocol === "https:") return Sites.secureDomainKey(tld.getDomain(url.hostname));
return url && url.origin || siteKey;
return Sites.origin(url) || siteKey;
}
static origin(site) {
try {
return new URL(site).origin;
let objUrl = site.href ? site : new URL(site);
let origin = objUrl.origin;
return origin === "null" ? objUrl.href : origin;
} catch (e) {};
return site;
return site.origin || site;
}
static toExternal(url) { // domains are stored in punycode internally
@ -101,6 +108,7 @@ var {Permissions, Policy, Sites} = (() => {
match(site) {
if (site && this.size) {
if (site instanceof URL) site = site.href;
if (this.has(site)) return site;
let {url, siteKey} = Sites.parse(site);

View File

@ -1,6 +1,6 @@
var PlaceHolder = (() => {
const HANDLERS = new Map();
let checkStyle = async () => {
checkStyle = () => {};
if (!ns.embeddingDocument) return;
@ -11,7 +11,7 @@ var PlaceHolder = (() => {
(await fetch(browser.extension.getURL("/content/content.css"))).text();
}
}
class Handler {
constructor(type, selector) {
this.type = type;
@ -20,10 +20,16 @@ var PlaceHolder = (() => {
HANDLERS.set(type, this);
}
filter(element, request) {
if (request.embeddingDocument) return true;
if (request.embeddingDocument) {
return document.URL === request.url;
}
let url = request.initialUrl || request.url;
return "data" in element ? element.data === url : element.src === url;
}
selectFor(request) {
return [...document.querySelectorAll(this.selector)]
.filter(element => this.filter(element, request))
}
}
new Handler("frame", "iframe");
@ -59,6 +65,9 @@ var PlaceHolder = (() => {
static canReplace(policyType) {
return HANDLERS.has(policyType);
}
static handlerFor(policyType) {
return HANDLERS.get(policyType);
}
static listen() {
PlaceHolder.listen = () => {};
@ -83,7 +92,7 @@ var PlaceHolder = (() => {
this.policyType = policyType;
this.request = request;
this.replacements = new Set();
this.handler = HANDLERS.get(policyType);
this.handler = PlaceHolder.handlerFor(policyType);
if (this.handler) {
[...document.querySelectorAll(this.handler.selector)]
.filter(element => this.handler.filter(element, request))
@ -100,7 +109,11 @@ var PlaceHolder = (() => {
let {
url
} = this.request;
this.origin = new URL(url).origin;
let objUrl = new URL(url)
this.origin = objUrl.origin;
if (this.origin === "null") {
this.origin = objUrl.protocol;
}
let TYPE = `<${this.policyType.toUpperCase()}>`;
let replacement = createHTMLElement("a");
@ -129,7 +142,7 @@ var PlaceHolder = (() => {
replacement._placeHolderObj = this;
replacement._placeHolderElement = element;
element.parentNode.replaceChild(replacement, element);
this.replacements.add(replacement);

View File

@ -1,6 +1,5 @@
'use strict';
// debug = () => {}; // REL_ONLY
var _ = browser.i18n.getMessage;
function createHTMLElement(name) {
@ -50,7 +49,7 @@ var notifyPage = async () => {
if (document.readyState === "complete") {
try {
if (!("canScript" in ns)) {
let childPolicy = await Messages.send("fetchChildPolicy", {url: document.URL, contextUrl: top.location.href});
let childPolicy = await Messages.send("fetchChildPolicy", {url: document.URL});
ns.config.CURRENT = childPolicy.CURRENT;
ns.setup(childPolicy.DEFAULT, childPolicy.MARKER);
return;
@ -82,11 +81,22 @@ ns.on("capabilities", () => {
},
allowed: ns.canScript
});
if (!ns.canScript) {
if (!ns.canScript) {
addEventListener("beforescriptexecute", e => e.preventDefault());
let mo = new MutationObserver(mutations => {
for (let m of mutations) {
console.log(`Mutation `, m);
if (m.type !== "attribute") continue;
if (/^on\w+/i.test(m.attributeName)) {
m.target.removeAttribute(m.attributeName);
} else if (/^\s*(javascript|data):/i.test(m.target.attributes[m.attributeName])) {
m.target.setAttribute(m.attributeName, "#");
}
}
});
// mo.observe(document.documentElement, {attributes: true, subtree: true});
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
addEventListener("beforescriptexecute", e => e.preventDefault());
(async () => {
for (let r of await navigator.serviceWorker.getRegistrations()) {
await r.unregister();
@ -97,6 +107,6 @@ ns.on("capabilities", () => {
if (document.readyState !== "loading") onScriptDisabled();
window.addEventListener("DOMContentLoaded", onScriptDisabled);
}
notifyPage();
});

View File

@ -1,18 +1,25 @@
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 request = {
id: `noscript-${policyType}-doc`,
type: policyType,
url: document.URL,
documentUrl: document.URL,
embeddingDocument: true,
};
if (ns.allows(policyType)) {
let handler = PlaceHolder.handlerFor(policyType);
if (handler && handler.selectFor(request).length > 0) {
seen.record({policyType, request, allowed: true});
}
} else {
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});
break;
}
}
}

View File

@ -1,4 +1,20 @@
function onScriptDisabled() {
if (document.URL.startsWith("file:")) {
// file: documents are loaded synchronously and may not be affected by
// CSP. We already intercept onbeforeexecutescript event, let's cope with
// event and URL attributes.
for (let e of document.all) {
for (let a of e.attributes) {
if (/^on\w+/i.test(a.name)) {
debug(`Removed %s.%sevent`, e.tagName, a.name);
a.value = "";
} else if (/^\s*(?:data|javascript):/i.test(unescape(a.value))) {
debug(`Neutralized %s.%s="%s" attribute`, e.tagName, a.name, a.value);
a.value = "data:";
}
}
}
}
for (let noscript of document.querySelectorAll("noscript")) {
// force show NOSCRIPT elements content
let replacement = createHTMLElement("span");

View File

@ -177,15 +177,13 @@ addEventListener("unload", e => {
let domains = new Map();
function urlToLabel(url) {
let {
origin
} = url;
let origin = Sites.origin(url);
let match = policySites.match(url);
if (match) return match;
if (domains.has(origin)) {
if (justDomains) return domains.get(origin);
} else {
let domain = tld.getDomain(url.hostname);
let domain = tld.getDomain(url.hostname) || origin;
domain = url.protocol === "https:" ? Sites.secureDomainKey(domain) : domain;
domains.set(origin, domain);
if (justDomains) return domain;
@ -196,7 +194,8 @@ addEventListener("unload", e => {
let parsedSeen = seen.map(thing => Object.assign({
type: thing.policyType
}, Sites.parse(thing.request.url)))
.filter(parsed => parsed.url && parsed.url.origin !== "null");
.filter(parsed => parsed.url && (
parsed.url.origin !== "null" || parsed.url.protocol === "file:"));
let sitesSet = new Set(
parsedSeen.map(parsed => parsed.label = urlToLabel(parsed.url))
@ -206,7 +205,7 @@ addEventListener("unload", e => {
}
let sites = [...sitesSet];
for (let parsed of parsedSeen) {
sites.filter(s => parsed.label === s || domains.get(parsed.url.origin) === s).forEach(m => {
sites.filter(s => parsed.label === s || domains.get(Sites.origin(parsed.url)) === s).forEach(m => {
let siteTypes = typesMap.get(m);
if (!siteTypes) typesMap.set(m, siteTypes = new Set());
siteTypes.add(parsed.type);