📝 Added entire config directory for backup purposes

This commit is contained in:
z3rOR0ne 2022-11-03 00:09:22 -07:00
parent 4b38e59bb6
commit 9b946e2d14
11091 changed files with 1440953 additions and 0 deletions

View file

@ -0,0 +1,60 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
'use strict';
var Defaults = {
async init() {
let defaults = {
local: {
debug: false,
showCtxMenuItem: true,
showCountBadge: true,
showFullAddresses: false,
amnesticUpdates: false,
},
sync: {
global: false,
xss: true,
cascadeRestrictions : false,
overrideTorBrowserPolicy: false, // note: Settings.update() on reset will flip this to true
}
};
let defaultsClone = JSON.parse(JSON.stringify(defaults));
for (let [k, v] of Object.entries(defaults)) {
let store = await Storage.get(k, k);
if (k in store) {
Object.assign(v, store[k]);
}
v.storage = k;
}
Object.assign(ns, defaults);
// dynamic settings
if (!ns.local.uuid) {
ns.local.uuid = uuid();
await ns.save(ns.local);
}
return ns.defaults = defaultsClone;
}
};

View file

@ -0,0 +1,348 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
"use strict";
var LifeCycle = (() => {
const AES = "AES-GCM",
keyUsages = ["encrypt", "decrypt"];
function toBase64(bytes) {
return btoa(Array.from(bytes).map(b => String.fromCharCode(b)).join(''));
}
function fromBase64(string) {
return Uint8Array.from((Array.from(atob(string)).map(c => c.charCodeAt(0))));
}
async function encrypt(clearText) {
let key = await crypto.subtle.generateKey({
name: AES,
length: 256,
},
true,
keyUsages,
);
let iv = crypto.getRandomValues(new Uint8Array(12));
let encoded = new TextEncoder().encode(clearText);
let cypherText = await crypto.subtle.encrypt({
name: AES,
iv
}, key, encoded);
return {cypherText, key: await crypto.subtle.exportKey("jwk", key), iv};
}
var LifeBoat = {
url: "about:blank",
async createAndStore() {
let allSeen = {};
let tab;
await Promise.all((await browser.tabs.query({})).map(
async t => {
let seen = await ns.collectSeen(t.id);
if (seen) {
allSeen[t.id] = seen;
if (!tab || !tab.incognito && t.incognito) {
tab = t;
}
}
}
));
if (!tab) { // no suitable existing tab, let's open a new one
if (!UA.isMozilla) {
// injecting new about:blank tabs is supported only by Mozilla: let's bailout
return;
}
let {url} = LifeBoat;
let tabInfo = {
url,
active: false,
};
if (browser.windows) { // it may be missing on mobile
// check if an incognito window exists and open our "survival" tab there
for (let w of await browser.windows.getAll()) {
if (w.incognito) {
tabInfo.windowId = w.id;
break;
}
}
}
for (;!tab;) {
try {
tab = await browser.tabs.create(tabInfo);
} catch (e) {
error(e);
if (tabInfo.windowId) {
// we might not have incognito permissions, let's try using any window
delete tabInfo.windowId;
} else {
return; // bailout
}
}
}
}
let tabId = tab.id;
let {url} = tab;
let {cypherText, key, iv} = await encrypt(JSON.stringify({
policy: ns.policy.dry(true),
allSeen,
unrestrictedTabs: [...ns.unrestrictedTabs]
}));
let attr;
try {
await new Promise((resolve, reject) => {
let l = async (tabId, changeInfo) => {
if (!!attr || tabId !== tab.id) return;
debug("Survival tab updating", changeInfo);
if (changeInfo.status !== "complete") return;
try {
attr = await Messages.send("store", {url, data: toBase64(new Uint8Array(cypherText))}, {tabId, frameId: 0});
resolve();
debug("Survival tab updated");
} catch (e) {
if (!Messages.isMissingEndpoint(e)) {
error(e, "Survival tab failed");
reject(e);
} // otherwise we keep waiting for further updates from the tab until content script is ready to answer
return false;
};
return true;
};
l(tabId, tab).then(r => {
if (!r) browser.tabs.onUpdated.addListener(l);
});
});
await Storage.set("local", { "updateInfo": {key, iv: toBase64(iv), tabId, url, attr}});
tabId = -1;
debug("Ready to reload...", await Storage.get("local", "updateInfo"));
} finally {
if (tabId !== -1 && url === LifeBoat.url && !ns.local.debug) {
browser.tabs.remove(tabId); // cleanup on failure unless we want to debug a post-mortem
}
}
},
async retrieveAndDestroy() {
let {updateInfo} = await Storage.get("local", "updateInfo");
if (!updateInfo) return;
await Storage.remove("local", "updateInfo");
let {key, iv, tabId, attr, url} = updateInfo;
let destroyIfNeeded = url === LifeBoat.url ? (keepIfDebug = false) => {
if (tabId === -1 || url !== LifeBoat.url) return;
if (keepIfDebug && ns.local.debug) {
debug("Failed survival tab %s left open for debugging.", tabId);
} else {
browser.tabs.remove(tabId);
}
tabId = -1;
} : () => {};
try {
key = await crypto.subtle.importKey("jwk", key, AES, true, keyUsages);
iv = fromBase64(iv);
let cypherText;
for (let attempts = 3; attempts-- > 0;) {
try {
cypherText = await Messages.send("retrieve", {url, attr}, {tabId, frameId: 0});
break;
} catch (e) {
if (Messages.isMissingEndpoint(e)) {
debug("Cannot retrieve survival tab data, maybe content script not loaded yet. Retrying...");
await ns.initializing;
await new Promise(resolve => setTimeout(resolve, 100));
} else {
throw e;
}
}
}
if (!cypherText) {
throw new Error("Could not retrieve survival tab data!");
}
cypherText = fromBase64(cypherText);
let encoded = await crypto.subtle.decrypt({
name: AES,
iv
}, key, cypherText
);
let {policy, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded));
if (!policy) {
throw new error("Ephemeral policy not found in survival tab %s!", tabId);
}
ns.unrestrictedTabs = new Set(unrestrictedTabs);
destroyIfNeeded();
if (ns.initializing) await ns.initializing;
ns.policy = new Policy(policy);
await Promise.all(
Object.entries(allSeen).map(
async ([tabId, seen]) => {
try {
debug("Restoring seen %o to tab %s", seen, tabId);
await Messages.send("allSeen", {seen}, {tabId, frameId: 0});
} catch (e) {
error(e, "Cannot send previously seen data to tab", tabId);
}
}
)
);
} catch (e) {
error(e);
} finally {
destroyIfNeeded(true);
}
}
}
return {
async onInstalled(details) {
browser.runtime.onInstalled.removeListener(this.onInstalled);
if (!UA.isMozilla) {
// Chromium does not inject content scripts at startup automatically for already loaded pages,
// let's hack it manually.
let contentScripts = browser.runtime.getManifest().content_scripts.find(s =>
s.js && s.matches.includes("<all_urls>") && s.all_frames && s.match_about_blank).js;
await Promise.all((await browser.tabs.query({})).map(async tab => {
for (let file of contentScripts) {
try {
await browser.tabs.executeScript(tab.id, {file, allFrames: true, matchAboutBlank: true});
} catch (e) {
await include("/nscl/common/restricted.js");
if (!isRestrictedURL(tab.url)) {
error(e, "Can't run content script on tab", tab);
}
break;
}
}
}));
}
let {reason, previousVersion} = details;
if (reason === "update") {
try {
await LifeBoat.retrieveAndDestroy();
} catch (e) {
error(e);
}
}
if (!previousVersion) return;
await include("/nscl/common/Ver.js");
previousVersion = new Ver(previousVersion);
let currentVersion = new Ver(browser.runtime.getManifest().version);
let upgrading = Ver.is(previousVersion, "<=", currentVersion);
if (!upgrading) return;
// put here any version specific upgrade adjustment in stored data
let forEachPreset = async (callback, presetNames = "*") => {
await ns.initializing;
let changed = false;
for (let p of ns.policy.getPresets(presetNames)) {
if (callback(p)) changed = true;
if (p.contextual) {
for (let ctxP of p.contextual.values()) {
if (callback(ctxP)) changed = true;
}
}
}
if (changed) {
await ns.savePolicy();
}
};
let configureNewCap = async (cap, presetNames, capsFilter) => {
log(`Upgrading from ${previousVersion}: configure the "${cap}" capability.`);
await forEachPreset(({capabilities}) => {
if (capsFilter(capabilities) && !capabilities.has(cap)) {
capabilities.add(cap);
return true;
}
}, presetNames);
};
let renameCap = async (oldName, newName) => {
log(`Upgrading from ${previousVersion}: rename capability "${oldName}" to "${newName}`);
await forEachPreset(({capabilities}) => {
if (capabilities.has(oldName)) {
capabilities.delete(oldName);
capabilities.add(newName);
return true;
}
});
};
if (Ver.is(previousVersion, "<=", "11.0.10")) {
await configureNewCap("ping", ["TRUSTED"]);
}
if (Ver.is(previousVersion, "<=", "11.2.1")) {
await configureNewCap("noscript", ["DEFAULT", "TRUSTED", "CUSTOM"])
}
if (Ver.is(previousVersion, "<=", "11.2.4")) {
// add the unchecked_css capability to any preset which already has the script capability
await configureNewCap("unchecked_css", ["DEFAULT", "TRUSTED", "CUSTOM"], caps => caps.has("script"));
}
if (Ver.is(previousVersion, "<=", "11.2.5rc1")) {
await renameCap("csspp0", "unchecked_css");
}
if (Ver.is(previousVersion, "<=", "11.3rc2")) {
// add the lan capability to any preset which already has the script capability
await configureNewCap("lan", ["DEFAULT", "TRUSTED", "CUSTOM"], caps => caps.has("script"));
}
if (Ver.is(previousVersion, "<=", "11.4.1rc3")) {
// show theme switcher on update unless user has already chosen between Vintage Blue and Modern Red
(async () => {
let isVintage = await Themes.isVintage();
if (typeof isVintage === "boolean") return;
await ns.init;
ns.openOptionsPage({tab: 2, focus: "#opt-vintageTheme", hilite: "#sect-themes"});
})();
}
},
async onUpdateAvailable(details) {
try {
if (ns.local.amnesticUpdates) {
// user doesn't want us to remember temporary settings across updates: bail out
return;
}
await include("/nscl/common/Ver.js");
if (Ver.is(details.version, "<", browser.runtime.getManifest().version)) {
// downgrade: temporary survival might not be supported, and we don't care
return;
}
await LifeBoat.createAndStore();
} catch (e) {
console.error(e);
} finally {
browser.runtime.reload(); // apply update
}
}
};
})();

View file

@ -0,0 +1,78 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
"use strict";
function ReportingCSP(marker, reportURI = "") {
const DOM_SUPPORTED = "SecurityPolicyViolationEvent" in window;
if (DOM_SUPPORTED) reportURI = "";
return Object.assign(
new CapsCSP(new NetCSP(
reportURI ? `report-uri ${reportURI}` : marker
)),
{
reportURI,
patchHeaders(responseHeaders, capabilities) {
let header = null;
let blocker;
if (capabilities) {
let contentType = responseHeaders.filter(h => h.name.toLowerCase() === "content-type");
let blockHTTP = contentType.length === 0 || contentType.some(h => !/^(?:text|application)\/\S*\b(?:x?ht|x)ml\b/i.test(h.value));
blocker = this.buildFromCapabilities(capabilities, blockHTTP);
}
let extras = [];
responseHeaders.forEach((h, index) => {
if (this.isMine(h)) {
header = h;
if (h.value === blocker) {
// make this equivalent but different than the original, otherwise
// it won't be (re)set when deleted, see
// https://dxr.mozilla.org/mozilla-central/rev/882de07e4cbe31a0617d1ae350236123dfdbe17f/toolkit/components/extensions/webrequest/WebRequest.jsm#138
blocker += " ";
} else {
extras.push(...this.unmergeExtras(h));
}
responseHeaders.splice(index, 1);
} else if (blocker && /^(Location|Refresh)$/i.test(h.name)) {
// neutralize any HTTP redirection to data: URLs, like Chromium
let url = /^R/i.test(h.name)
? h.value.replace(/^[^,;]*[,;](?:\W*url[^=]*=)?[^!#$%&()*+,/:;=?@[\]\w.,~-]*/i, "") : h.value;
if (/^data:/i.test(url)) {
h.value = h.value.slice(0, -url.length) + "data:";
}
}
});
if (blocker) {
header = this.asHeader(blocker);
responseHeaders.push(header);
}
if (extras.length) {
responseHeaders.push(...extras);
}
return header;
}
}
);
}

View file

@ -0,0 +1,820 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2022 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
var RequestGuard = (() => {
'use strict';
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
browser.browserAction.setTitle({title: VERSION_LABEL});
const CSP_REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
const CSP_MARKER = "noscript-marker";
let csp = new ReportingCSP(CSP_MARKER, CSP_REPORT_URI);
const policyTypesMap = {
main_frame: "",
sub_frame: "frame",
script: "script",
xslt: "script",
xbl: "script",
font: "font",
object: "object",
object_subrequest: "fetch",
xmlhttprequest: "fetch",
ping: "ping",
beacon: "ping",
media: "media",
other: "",
};
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
const TabStatus = {
map: new Map(),
_originsCache: new Map(),
types: ["script", "object", "media", "frame", "font"],
newRecords() {
return {
allowed: {},
blocked: {},
noscriptFrames: {},
origins: new Set(),
}
},
hasOrigin(tabId, url) {
let records = this.map.get(tabId);
return records && records.origins.has(Sites.origin(url));
},
addOrigin(tabId, url) {
if (tabId < 0) return;
let origin = Sites.origin(url);
if (!origin) return;
let {origins} = this.map.get(tabId) || this.initTab(tabId);
if (!origins.has(origin)) {
origins.add(origin);
this._originsCache.clear();
}
},
findTabsByOrigin(origin) {
let tabIds = this._originsCache.get(origin);
if (!tabIds) {
tabIds = [];
for(let [tabId, {origins}] of [...this.map]) {
if (origins.has(origin)) tabIds.push(tabId);
}
this._originsCache.set(origin, tabIds);
}
return tabIds;
},
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;
let requestKey = Policy.requestKey(url, policyType, documentUrl);
let {map} = this;
let records = map.has(tabId) ? map.get(tabId) : this.initTab(tabId);
if (what === "noscriptFrame" && type !== "object") {
let nsf = records.noscriptFrames;
nsf[frameId] = optValue;
what = optValue ? "blocked" : "allowed";
if (frameId === 0) {
request.type = type = "main_frame";
Content.reportTo(request, optValue, type);
}
}
if (type.endsWith("frame")) {
this.addOrigin(tabId, url);
} else if (documentUrl) {
this.addOrigin(tabId, documentUrl);
}
let collection = records[what];
if (collection) {
if (type in collection) {
if (!collection[type].includes(requestKey)) {
collection[type].push(requestKey);
}
} else {
collection[type] = [requestKey];
}
}
return records;
},
record(request, what, optValue) {
let {tabId} = request;
if (tabId < 0) return;
let records = this._record(request, what, optValue);
if (records) {
this.updateTab(request.tabId);
}
},
_pendingTabs: new Set(),
updateTab(tabId) {
if (tabId < 0) return;
if (this._pendingTabs.size === 0) {
window.setTimeout(() => { // clamp UI updates
for (let tabId of this._pendingTabs) {
this._updateTabNow(tabId);
}
this._pendingTabs.clear();
}, 200);
}
this._pendingTabs.add(tabId);
},
_updateTabNow(tabId) {
this._pendingTabs.delete(tabId);
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 = enforced ?
(topAllowed ? (numBlocked ? "part" : "yes")
: (numAllowed ? "sub" : "no")) // not topAllowed
: "global"; // not enforced
let showBadge = ns.local.showCountBadge && numBlocked > 0;
let browserAction = browser.browserAction;
if (!browserAction.setIcon) { // Fennec
browserAction.setTitle({tabId, title: `NoScript (${numBlocked})`});
return;
}
(async () => {
let iconPath = (await Themes.isVintage()) ? '/img/vintage' : '/img';
browserAction.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}});
})();
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
browserAction.setTitle({tabId,
title: UA.mobile ? "NoScript" : `${VERSION_LABEL} \n${enforced ?
_("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}`
: _("NotEnforced")}`
});
},
async probe(tabId) {
if (tabId === undefined) {
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
} else {
try {
TabStatus.recordAll(tabId, await ns.collectSeen(tabId));
} catch (e) {
error(e);
}
}
},
recordAll(tabId, seen) {
if (seen) {
let records = TabStatus.map.get(tabId);
if (records) {
records.allowed = {};
records.blocked = {};
}
for (let thing of seen) {
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);
},
onUpdatedTab(tabId, changeInfo) {
if (changeInfo.url) {
TabStatus.initTab(tabId);
}
},
onRemovedTab(tabId) {
TabStatus.map.delete(tabId);
TabStatus._originsCache.clear();
TabStatus._pendingTabs.delete(tabId);
},
}
for (let event of ["Activated", "Updated", "Removed"]) {
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
}
let messageHandler = {
async pageshow(message, sender) {
if (sender.frameId === 0) {
TabStatus.recordAll(sender.tab.id, message.seen);
}
return true;
},
violation({url, type}, sender) {
let tabId = sender.tab.id;
let {frameId} = sender;
let r = {
url, type, tabId, frameId, documentUrl: sender.url
};
Content.reportTo(r, false, policyTypesMap[type]);
debug("Violation", type, url, tabId, frameId);
if (type === "script" && url === sender.url) {
TabStatus.record(r, "noscriptFrame", true);
} else {
TabStatus.record(r, "blocked");
}
},
async blockedObjects(message, sender) {
let {url, documentUrl, policyType} = message;
let TAG = `<${policyType.toUpperCase()}>`;
let origin = Sites.origin(url);
let {siteKey} = Sites.parse(url);
let options;
if (siteKey === origin) {
origin = new URL(url).protocol;
}
options = [
{label: _("allowLocal", siteKey), checked: true},
{label: _("allowLocal", origin)},
{label: _("CollapseBlockedObjects")},
];
let t = u => `${TAG}@${u}`;
let ret = await Prompts.prompt({
title: _("BlockedObjects"),
message: _("allowLocal", TAG),
options});
debug(`Prompt returned`, ret, sender);
if (ret.button !== 0) return;
if (ret.option === 2) {
return {collapse: "all"};
}
let key = [siteKey, origin][ret.option || 0];
if (!key) return;
let contextUrl = sender.tab.url || documentUrl;
let {siteMatch, contextMatch, perms} = ns.policy.get(key, contextUrl);
let {capabilities} = perms;
if (!capabilities.has(policyType)) {
let temp = sender.tab.incognito; // we don't want to store in PBM
perms = new Permissions(new Set(capabilities), temp);
perms.capabilities.add(policyType);
/* TODO: handle contextual permissions
if (contextUrl) {
let context = Sites.optimalKey(contextUrl);
let contextualSites = new Sites([[context, perms]]);
perms = new Permissions(new Set(capabilities), false, contextualSites);
}
*/
ns.policy.set(key, perms);
await ns.savePolicy();
}
return {enable: key};
},
}
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
let initialUrl = pending ? pending.initialUrl : request.url;
request = {
key: Policy.requestKey(url, type, documentUrl || "", /^(media|object|frame)$/.test(type)),
type, url, documentUrl, originUrl
};
if (tabId < 0) {
if ((policyType === "script" || policyType === "fetch") &&
url.startsWith("https://") && documentUrl && documentUrl.startsWith("https://")) {
// service worker request ?
let payload = {request, allowed, policyType, serviceWorker: Sites.origin(documentUrl)};
let recipient = {frameId: 0};
for (let tabId of TabStatus.findTabsByOrigin(payload.serviceWorker)) {
recipient.tabId = tabId;
try {
Messages.send("seen", payload, recipient);
} catch (e) {
// likely a privileged tab where our content script couldn't run
}
}
}
return;
}
if (pending) request.initialUrl = pending.initialUrl;
if (type !== "sub_frame") { // we couldn't deliver it to frameId, since it's generally not loaded yet
try {
await Messages.send("seen",
{request, allowed, policyType, ownFrame: true},
{tabId, frameId}
);
} catch (e) {
debug(`Couldn't deliver "seen" message for ${type}@${url} ${allowed ? "A" : "F" } to document ${documentUrl} (${frameId}/${tabId})`, e);
}
}
if (frameId === 0) return;
try {
await Messages.send("seen",
{request, allowed, policyType},
{tabId, frameId: 0}
);
} catch (e) {
debug(`Couldn't deliver "seen" message to top frame containing ${documentUrl} (${frameId}/${tabId}`, e);
}
}
};
const pendingRequests = new Map();
function initPendingRequest(request) {
let {requestId, url} = request;
let redirected = pendingRequests.get(requestId);
let initialUrl = redirected ? redirected.initialUrl : url;
pendingRequests.set(requestId, {
initialUrl, url, redirected,
onCompleted: new Set(),
});
return redirected;
}
let normalizeRequest = UA.isMozilla ? () => {} : request => {
if ("initiator" in request && !("originUrl" in request)) {
request.originUrl = request.initiator;
if (request.type !== "main_frame" && !("documentUrl" in request)) {
request.documentUrl = request.initiator;
}
}
};
function intersectCapabilities(perms, request) {
let {frameId, frameAncestors, tabId} = request;
if (frameId !== 0 && ns.sync.cascadeRestrictions) {
let topUrl = frameAncestors && frameAncestors.length
&& frameAncestors[frameAncestors.length - 1].url;
if (!topUrl) {
let tab = TabCache.get(tabId);
if (tab) topUrl = tab.url;
}
if (topUrl) {
return ns.policy.cascadeRestrictions(perms, topUrl).capabilities;
}
}
return perms.capabilities;
}
const ABORT = {cancel: true}, ALLOW = {};
const recent = {
MAX_AGE: 500,
_pendingGC: 0,
_byUrl: new Map(),
find(request, last = this._byUrl.get(request.url)) {
if (!last) return null;
for (let j = last.length; j-- > 0;) {
let other = last[j];
if (request.timeStamp - other.timeStamp > this.MAX_AGE) {
last.splice(0, ++j);
if (last.length === 0) this._byUrl.delete(other.url);
break;
}
if (request.url && other.type === request.type && other.documentUrl === request.documentUrl
&& other.tabId === request.tabId && other.frameId === request.frameId) {
return other;
}
}
return null;
},
add(request) {
let last = this._byUrl.get(request.url);
if (!last) {
last = [request];
this._byUrl.set(request.url, last);
} else {
last.push(request);
}
this._gc();
return;
},
_gc(now) {
if (!now && this._pendingGC) return;
debug("Recent requests garbage collection.");
let request = {timeStamp: Date.now()};
for (let last of this._byUrl.values()) {
this.find(request, last);
}
this._pendingGC = this._byUrl.size ?
setTimeout(() => this._gc(true), 1000)
: 0;
}
};
function blockLANRequest(request) {
debug("WAN->LAN request blocked", request);
let r = Object.assign({}, request);
r.url = request.originUrl; // we want to report the origin as needing the permission
Content.reportTo(r, false, "lan")
return ABORT;
}
function checkLANRequest(request) {
if (!ns.isEnforced(request.tabId)) return ALLOW;
let {originUrl, url} = request;
if (originUrl && !Sites.isInternal(originUrl) && url.startsWith("http") &&
!ns.policy.can(originUrl, "lan", ns.policyContext(request))) {
// we want to block any request whose origin resolves to at least one external WAN IP
// and whose destination resolves to at least one LAN IP
let {proxyInfo} = request; // see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo
let neverDNS = (proxyInfo && (proxyInfo.type && proxyInfo.type.startsWith("http") || proxyInfo.proxyDNS))
|| !(UA.isMozilla && DNS.supported);
if (neverDNS) {
// On Chromium we must do it synchronously: we need to sacrifice DNS resolution and check just numeric addresses :(
// (the Tor Browser, on the other hand, does DNS resolution and boundary checks on its own and breaks the DNS API)
return iputil.isLocalURI(url, false, neverDNS) && !iputil.isLocalURI(originUrl, true, neverDNS)
? blockLANRequest(request)
: ALLOW;
}
// Firefox does support asynchronous webRequest: let's return a Promise and perform DNS resolution.
return new Promise(async (resolve, reject) => {
try {
resolve(await iputil.isLocalURI(url, false) && !(await iputil.isLocalURI(originUrl, true))
? blockLANRequest(request)
: ALLOW
);
} catch (e) {
reject(e);
}
});
}
}
const listeners = {
onBeforeRequest(request) {
try {
if (browser.runtime.onSyncMessage && browser.runtime.onSyncMessage.isMessageRequest(request)) return ALLOW;
normalizeRequest(request);
initPendingRequest(request);
let {policy} = ns
let {tabId, type, url, originUrl} = request;
if (type in policyTypesMap) {
let previous = recent.find(request);
if (previous) {
debug("Rapid fire request", previous);
return previous.return;
}
(previous = request).return = ALLOW;
recent.add(previous);
let policyType = policyTypesMap[type];
let {documentUrl} = request;
if (!ns.isEnforced(tabId)) {
if (ns.unrestrictedTabs.has(tabId) && type.endsWith("frame") && url.startsWith("https:")) {
TabStatus.addOrigin(tabId, url);
}
if (type !== "main_frame") {
Content.reportTo(request, true, policyType);
}
return ALLOW;
}
let isFetch = "fetch" === policyType;
if ((isFetch || "frame" === policyType) &&
(((isFetch && !originUrl
|| url === originUrl) && originUrl === documentUrl
// some extensions make them both undefined,
// see https://github.com/eight04/image-picka/issues/150
) ||
Sites.isInternal(originUrl))
) {
// livemark request or similar browser-internal, always allow;
return ALLOW;
}
if (/^(?:data|blob):/.test(url)) {
request._dataUrl = url;
request.url = url = documentUrl || originUrl;
}
let allowed = Sites.isInternal(url);
if (!allowed) {
if (tabId < 0 && documentUrl && documentUrl.startsWith("https:")) {
allowed = [...ns.unrestrictedTabs]
.some(tabId => TabStatus.hasOrigin(tabId, documentUrl));
}
if (!allowed) {
let capabilities = intersectCapabilities(
policy.get(url, ns.policyContext(request)).perms,
request);
allowed = !policyType || capabilities.has(policyType);
if (allowed && request._dataUrl && type.endsWith("frame")) {
let blocker = csp.buildFromCapabilities(capabilities);
if (blocker) {
let redirectUrl = CSP.patchDataURI(request._dataUrl, blocker);
if (redirectUrl !== request._dataUrl) {
return previous.return = {redirectUrl};
}
}
}
}
}
if (type !== "main_frame") {
Content.reportTo(request, allowed, policyType);
}
if (!allowed) {
debug(`Blocking ${policyType}`, request);
TabStatus.record(request, "blocked");
return previous.return = ABORT;
}
}
} catch (e) {
error(e);
}
return ALLOW;
},
onBeforeSendHeaders(request) {
normalizeRequest(request);
return checkLANRequest(request);
},
onHeadersReceived(request) {
// called for main_frame, sub_frame and object
// check for duplicate calls
let pending = pendingRequests.get(request.requestId);
if (pending) {
if (pending.headersProcessed) {
if (!request.fromCache) {
debug("Headers already processed, skipping ", request);
return ALLOW;
}
debug("Reprocessing headers for cached request ", request);
} else {
debug("onHeadersReceived", request);
}
} else {
debug("[WARNING] no pending information for ", request);
initPendingRequest(request);
pending = pendingRequests.get(request.requestId);
}
if (request.fromCache && listeners.onHeadersReceived.resetCSP && !pending.resetCachedCSP) {
debug("Resetting CSP Headers");
pending.resetCachedCSP = true;
let {responseHeaders} = request;
let headersCount = responseHeaders.length;
let purged = false;
responseHeaders.forEach((h, index) => {
if (csp.isMine(h)) {
responseHeaders.splice(index, 1);
}
});
if (headersCount > responseHeaders.length) {
debug("Resetting cached NoScript CSP header(s)", request);
return {responseHeaders};
}
}
normalizeRequest(request);
let result = ALLOW;
let promises = [];
let headersModified = false;
pending.headersProcessed = true;
let {url, documentUrl, tabId, responseHeaders, type} = request;
let isMainFrame = type === "main_frame";
try {
let capabilities;
if (ns.isEnforced(tabId)) {
let policy = ns.policy;
let {perms} = policy.get(url, ns.policyContext(request));
if (isMainFrame) {
if (policy.autoAllowTop && perms === policy.DEFAULT) {
policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin);
}
capabilities = perms.capabilities;
} else {
capabilities = intersectCapabilities(perms, request);
}
} // else unrestricted, either globally or per-tab
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);
/*
// Uncomment me to disable networking-level CSP for debugging purposes
header = null;
*/
if (header) {
pending.cspHeader = header;
debug(`CSP blocker on %s:`, url, header.value);
headersModified = true;
}
if (headersModified) {
result = {responseHeaders};
debug("Headers changed ", request);
}
} catch (e) {
error(e, "Error in onHeadersReceived", request);
}
promises = promises.filter(p => p instanceof Promise);
if (promises.length > 0) {
return Promise.all(promises).then(() => result);
}
return result;
},
onResponseStarted(request) {
normalizeRequest(request);
debug("onResponseStarted", request);
let {requestId, url, tabId, frameId, type} = request;
if (type === "main_frame") {
TabStatus.initTab(tabId);
}
let scriptBlocked = request.responseHeaders.some(
h => csp.isMine(h) && csp.blocks(h.value, "script")
);
debug("%s scriptBlocked=%s setting noscriptFrame on ", url, scriptBlocked, tabId, frameId);
TabStatus.record(request, "noscriptFrame", scriptBlocked);
let pending = pendingRequests.get(requestId);
if (pending) {
pending.scriptBlocked = scriptBlocked;
if (!(pending.headersProcessed &&
(scriptBlocked || ns.requestCan(request, "script"))
)) {
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId,
pending.headersProcessed ? "has been overridden on": "could not process",
request);
}
}
},
onCompleted(request) {
let {requestId} = request;
if (pendingRequests.has(requestId)) {
let r = pendingRequests.get(requestId);
pendingRequests.delete(requestId);
for (let callback of r.onCompleted) {
try {
callback(request, r);
} catch (e) {
error(e);
}
}
}
},
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";
let url = report['blocked-uri'];
if (!url || url === 'self') url = request.documentUrl;
return Object.assign({}, request, {
url,
type,
});
}
let utf8Decoder = new TextDecoder("UTF-8");
function onViolationReport(request) {
try {
let text = utf8Decoder.decode(request.requestBody.raw[0].bytes);
if (text.includes(`"inline"`)) return ABORT;
let report = JSON.parse(text)["csp-report"];
let originalPolicy = report["original-policy"]
debug("CSP report", report);
let blockedURI = report['blocked-uri'];
if (blockedURI && blockedURI !== 'self') {
let r = fakeRequestFromCSP(report, request);
if (!/:/.test(r.url)) r.url = request.documentUrl;
Content.reportTo(r, false, policyTypesMap[r.type]);
TabStatus.record(r, "blocked");
} else if (report["violated-directive"].startsWith("script-src") && (originalPolicy.includes("script-src 'none'"))) {
let r = fakeRequestFromCSP(report, request);
Content.reportTo(r, false, "script"); // NEW
TabStatus.record(r, "noscriptFrame", true);
}
} catch(e) {
error(e);
}
return ABORT;
}
function injectPolicyScript(details) {
let {url, tabId, frameId} = details;
let policy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
policy.navigationURL = url;
let debugStatement = ns.local.debug ? `
let mark = Date.now() + ":" + Math.random();
console.debug("domPolicy", domPolicy, document.readyState, location.href, mark, window.ns);` : '';
return `
let domPolicy = ${JSON.stringify(policy)};
let {ns} = window;
if (ns) {
ns.domPolicy = domPolicy;
if (ns.setup) {
if (ns.syncSetup) ns.syncSetup(domPolicy);
else ns.setup(domPolicy);
} ;
} else {
window.ns = {domPolicy}
}
${debugStatement}`;
}
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};
listen("onBeforeRequest", filterAll, ["blocking"]);
listen("onBeforeSendHeaders", filterAll, ["blocking"]);
let mergingCSP = "getBrowserInfo" in browser.runtime;
if (mergingCSP) {
let {vendor, version} = await browser.runtime.getBrowserInfo();
mergingCSP = vendor === "Mozilla" && parseInt(version) >= 77;
}
if (mergingCSP) {
// In Gecko>=77 (https://bugzilla.mozilla.org/show_bug.cgi?id=1462989)
// we need to cleanup our own cached headers in a dedicated listener :(
// see also https://trac.torproject.org/projects/tor/ticket/34305
wr.onHeadersReceived.addListener(
listeners.onHeadersReceived.resetCSP = request => {
return listeners.onHeadersReceived(request);
}, filterDocs, ["blocking", "responseHeaders"]);
}
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
// Still, other extensions may accidentally delete our CSP header
// if called before us, hence we try our best reinjecting it in the end
(listeners.onHeadersReceivedLast =
new LastListener(wr.onHeadersReceived, request => {
let {requestId, responseHeaders} = request;
let pending = pendingRequests.get(request.requestId);
if (pending && pending.headersProcessed) {
let {cspHeader} = pending;
if (cspHeader) {
responseHeaders.push(cspHeader);
return {responseHeaders};
}
} else {
debug("[WARNING] onHeadersReceived not called (yet?)", request);
}
return ALLOW;
}, filterDocs, ["blocking", "responseHeaders"])).install();
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
listen("onCompleted", filterAll);
listen("onErrorOccurred", filterAll);
if (csp.reportURI) {
wr.onBeforeRequest.addListener(onViolationReport,
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
}
DocStartInjection.register(injectPolicyScript);
TabStatus.probe();
},
stop() {
let wr = browser.webRequest;
for (let [name, listener] of Object.entries(listeners)) {
if (typeof listener === "function") {
wr[name].removeListener(listener);
} else if (listener instanceof LastListener) {
listener.uninstall();
}
}
wr.onBeforeRequest.removeListener(onViolationReport);
if (listeners.onHeadersReceived.resetCSP) {
wr.onHeadersReceived.removeListener(listeners.onHeadersReceived.resetCSP);
}
DocStartInjection.unregister(injectPolicyScript);
Messages.removeHandler(messageHandler);
}
};
return RequestGuard;
})();

View file

@ -0,0 +1,233 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
var Settings = {
async import(data) {
// figure out whether it's just a whitelist, a legacy backup or a "Quantum" export
try {
let json = JSON.parse(data);
if (json.whitelist) {
return await this.importLegacy(json);
}
if (json.trusted) {
return await this.importPolicy(json);
}
if (json.policy) {
return await this.importSettings(json);
}
} catch (e) {
if (data.includes("[UNTRUSTED]")) await this.importLists(data);
else throw e;
}
},
async importLegacy(json) {
await include("/legacy/Legacy.js");
if (await Legacy.import(json)) {
try {
ns.policy = Legacy.migratePolicy();
await ns.savePolicy();
await Legacy.persist();
return true;
} catch (e) {
error(e, "Importing legacy settings");
Legacy.migrated = Legacy.undo;
}
}
return false;
},
async importLists(data) {
await include("/legacy/Legacy.js");
try {
let [trusted, untrusted] = Legacy.extractLists(data.split("[UNTRUSTED]"));
let policy = ns.policy;
for (let site of trusted) {
policy.set(site, policy.TRUSTED);
}
for (let site of untrusted) {
policy.set(site, policy.UNTRUSTED, true);
}
await ns.savePolicy();
} catch (e) {
error(e, "Importing white/black lists %s", data);
return false;
}
return true;
},
async importPolicy(json) {
try {
ns.policy = new Policy(json);
await ns.savePolicy();
return true;
} catch (e) {
error(e, "Importing policy %o", json);
}
},
async importSettings(json) {
try {
await this.update(json);
return true;
} catch (e) {
error(e, "Importing settings %o", json);
}
return false;
},
async update(settings) {
let {
policy,
xssUserChoices,
tabId,
unrestrictedTab,
reloadAffected,
isTorBrowser,
} = settings;
debug("Received settings ", settings);
let oldDebug = ns.local.debug;
let reloadOptionsUI = false;
if (isTorBrowser) {
// Tor Browser-specific settings
ns.defaults.local.isTorBrowser = true; // prevents reset from forgetting
ns.defaults.sync.cascadeRestrictions = true; // we want this to be the default even on reset
Sites.onionSecure = true;
if (!this.gotTorBrowserInit) {
// First initialization message from the Tor Browser
this.gotTorBrowserInit = true;
if (ns.sync.overrideTorBrowserPolicy) {
// If the user chose to override Tor Browser's policy we skip
// copying the Security Level preset on startup (only).
// Manually changing the security level works as usual.
ns.local.isTorBrowser = true;
await ns.save(ns.local);
return;
}
} else {
reloadOptionsUI = true;
}
let torBrowserSettings = {
local: {
isTorBrowser: true,
},
sync: {
cascadeRestrictions: true,
}
}
for (let [storage, prefs] of Object.entries(torBrowserSettings)) {
settings[storage] = Object.assign(settings[storage] || {}, prefs);
}
}
if (settings.sync === null) {
// user is resetting options
policy = this.createDefaultDryPolicy();
// overriden defaults when user manually resets options
// we want the reset options to stick (otherwise it gets very confusing)
ns.defaults.sync.overrideTorBrowserPolicy = true;
reloadOptionsUI = true;
}
await Promise.all(["local", "sync"].map(
async storage => (settings[storage] || // changed or...
settings[storage] === null // ... needs reset to default
) && await ns.save(settings[storage]
? Object.assign(ns[storage], settings[storage]) : ns[storage] = Object.assign({}, ns.defaults[storage]))
));
if (ns.local.debug !== oldDebug) {
await include("/nscl/common/log.js");
if (oldDebug) debug = () => {};
}
if (policy) {
ns.policy = new Policy(policy);
await ns.savePolicy();
}
if (typeof unrestrictedTab === "boolean") {
ns.unrestrictedTabs[unrestrictedTab ? "add" : "delete"](tabId);
}
if (reloadAffected) {
browser.tabs.reload(tabId);
}
if (xssUserChoices) await XSS.saveUserChoices(xssUserChoices);
if (ns.sync.xss) {
XSS.start();
} else {
XSS.stop();
}
if (reloadOptionsUI) await this.reloadOptionsUI();
},
createDefaultDryPolicy() {
let dp = new Policy().dry();
dp.sites.trusted = `
addons.mozilla.org
afx.ms ajax.aspnetcdn.com
ajax.googleapis.com bootstrapcdn.com
code.jquery.com firstdata.com firstdata.lv gfx.ms
google.com googlevideo.com gstatic.com
hotmail.com live.com live.net
maps.googleapis.com mozilla.net
netflix.com nflxext.com nflximg.com nflxvideo.net
noscript.net
outlook.com passport.com passport.net passportimages.com
paypal.com paypalobjects.com
securecode.com securesuite.net sfx.ms tinymce.cachefly.net
wlxrs.com
yahoo.com yahooapis.com
yimg.com youtube.com ytimg.com
`.trim().split(/\s+/).map(Sites.secureDomainKey);
return dp;
},
export() {
return JSON.stringify({
policy: ns.policy.dry(),
local: ns.local,
sync: ns.sync,
xssUserChoices: XSS.getUserChoices(),
}, null, 2);
},
async reloadOptionsUI() {
try {
for (let t of await browser.tabs.query({url: browser.runtime.getURL(
browser.runtime.getManifest().options_ui.page) })
) {
browser.tabs.reload(t.id);
};
} catch (e) {
error(e);
}
}
}

View file

@ -0,0 +1,88 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
function deferWebTraffic(promiseToWaitFor, next) {
debug("deferWebTraffic on %o", promiseToWaitFor);
let seenTabs = new Set();
function checkNavigation(nav) {
if (nav.tabId !== browser.tabs.TAB_ID_NONE && nav.url.startsWith("http")) {
let seen = seenTabs.has(nav.tabId);
debug(`%s navigation %o`, seen ? "seen" : "unseen", nav);
if (!seen) {
reloadTab(nav.tabId);
}
}
}
browser.webNavigation.onCommitted.addListener(checkNavigation);
function reloadTab(tabId) {
seenTabs.add(tabId);
try {
browser.tabs.executeScript(tabId, {
runAt: "document_start",
code: "if (performance.now() < 60000) window.location.reload(false)"
});
debug("Reloading tab", tabId);
} catch (e) {
error(e, "Can't reload tab", tabId);
}
}
async function waitFor(request) {
let {type, documentUrl, url, tabId, frameId} = request;
if (tabId === browser.tabs.TAB_ID_NONE) return;
if (!seenTabs.has(tabId)) {
if (type === "main_frame") {
seenTabs.add(tabId);
} else if (documentUrl) {
if (frameId !== 0 && request.frameAncestors) {
documentUrl = request.frameAncestors.pop().url;
}
reloadTab(tabId);
}
}
debug("Deferring %s %s from %s", type, url, documentUrl);
try {
await promiseToWaitFor;
} catch (e) {
error(e);
}
debug("Green light to %s %s from %s", type, url, documentUrl);
}
function spyTabs(request) {
debug("Spying request %o", request);
}
browser.webRequest.onHeadersReceived.addListener(spyTabs, {
urls: ["<all_urls>"],
types: ["main_frame"],
}, ["blocking", "responseHeaders"]);
browser.webRequest.onBeforeRequest.addListener(waitFor, {
urls: ["<all_urls>"]
}, ["blocking"]);
(async () => {
await promiseToWaitFor;
browser.webNavigation.onCommitted.removeListener(checkNavigation);
browser.webRequest.onBeforeRequest.removeListener(waitFor);
browser.webRequest.onHeadersReceived.removeListener(spyTabs);
if (next) next();
})();
}

View file

@ -0,0 +1,384 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
{
'use strict';
{
for (let event of ["onInstalled", "onUpdateAvailable"]) {
browser.runtime[event].addListener(async details => {
await include("/bg/LifeCycle.js");
LifeCycle[event](details);
});
}
}
let popupURL = browser.runtime.getURL("/ui/popup.html");
let popupFor = tabId => `${popupURL}#tab${tabId}`;
let ctxMenuId = "noscript-ctx-menu";
async function toggleCtxMenuItem(show = ns.local.showCtxMenuItem) {
if (!("contextMenus" in browser)) return;
let id = ctxMenuId;
try {
await browser.contextMenus.remove(id);
} catch (e) {}
if (show) {
browser.contextMenus.create({
id,
title: "NoScript",
contexts: ["all"]
});
}
}
async function init() {
await Defaults.init();
if (!ns.policy) { // it could have been already retrieved by LifeCycle
let policyData = (await Storage.get("sync", "policy")).policy;
if (policyData && policyData.DEFAULT) {
ns.policy = new Policy(policyData);
if (ns.local.enforceOnRestart && !ns.policy.enforced) {
ns.policy.enforced = true;
await ns.savePolicy();
}
} else {
await include("/legacy/Legacy.js");
ns.policy = await Legacy.createOrMigratePolicy();
await ns.savePolicy();
}
}
let {isTorBrowser} = ns.local;
Sites.onionSecure = isTorBrowser;
if (!isTorBrowser) {
await include("/nscl/service/prefetchCSSResources.js");
}
await RequestGuard.start();
await XSS.start(); // we must start it anyway to initialize sub-objects
if (!ns.sync.xss) {
XSS.stop();
}
Messages.addHandler(messageHandler);
try {
await Messages.send("started");
} catch (e) {
// no embedder to answer us
}
log("STARTED");
await include("/bg/popupHandler.js");
};
let Commands = {
async openPageUI() {
if (ns.popupOpening) return;
ns.popupOpening = true;
ns.popupOpened = false;
let openPanel = async () => {
ns.popupOpening = false;
if (ns.popupOpened) return;
messageHandler.openStandalonePopup();
};
try {
await browser.browserAction.openPopup();
setTimeout(openPanel, 500);
return;
} catch (e) {
openPanel();
debug(e);
}
},
togglePermissions() {},
install() {
if ("command" in browser) {
// keyboard shortcuts
browser.commands.onCommand.addListener(cmd => {
if (cmd in Commands) {
Commands[cmd]();
}
});
}
if ("contextMenus" in browser) {
toggleCtxMenuItem();
browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId == ctxMenuId) {
this.openPageUI();
}
});
}
// wiring main UI
let ba = browser.browserAction;
if ("setIcon" in ba) {
//desktop or Fenix
ba.setPopup({
popup: popupURL
});
} else {
// Fennec
ba.onClicked.addListener(async tab => {
try {
await browser.tabs.remove(await browser.tabs.query({
url: popupURL
}));
} catch (e) {}
await browser.tabs.create({
url: popupFor(tab.id)
});
});
}
}
}
let messageHandler = {
async updateSettings(settings, sender) {
await Settings.update(settings);
toggleCtxMenuItem();
},
async broadcastSettings({
tabId = -1
}) {
let policy = ns.policy.dry(true);
let seen = tabId !== -1 ? await ns.collectSeen(tabId) : null;
let xssUserChoices = await XSS.getUserChoices();
await Messages.send("settings", {
policy,
seen,
xssUserChoices,
local: ns.local,
sync: ns.sync,
unrestrictedTab: ns.unrestrictedTabs.has(tabId),
tabId,
xssBlockedInTab: XSS.getBlockedInTab(tabId),
});
},
async exportSettings() {
return Settings.export();
},
async importSettings({data}) {
return await Settings.import(data);
},
async fetchChildPolicy({url, contextUrl}, sender) {
await ns.initializing;
return (messageHandler.fetchChildPolicy =
ns.computeChildPolicy)(...arguments);
},
async openStandalonePopup() {
let [tab] = (await browser.tabs.query({
currentWindow: true,
active: true
}));
if (!tab || tab.id === -1) {
log("No tab found to open the UI for");
return;
}
let win = await browser.windows.getCurrent();
browser.windows.create({
url: popupFor(tab.id),
width: 800,
height: 600,
top: win.top + 48,
left: win.left + 48,
type: "panel"
});
},
};
function onSyncMessage(msg, sender) {
switch(msg.id) {
case "fetchChildPolicy":
return messageHandler.fetchChildPolicy(msg, sender);
break;
}
}
var ns = {
running: false,
policy: null,
local: null,
sync: null,
initializing: null,
unrestrictedTabs: new Set(),
isEnforced(tabId = -1) {
return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId));
},
policyContext(contextualData) {
// contextualData (e.g. a request details object) must contain a tab, a tabId or a documentUrl
// (used as a fallback if tab's top URL cannot be retrieved, e.g. in service workers)
let {tab, tabId, documentUrl, url} = contextualData;
if (!tab) {
if (contextualData.type === "main_frame") return url;
tab = tabId !== -1 && TabCache.get(tabId);
}
return tab && tab.url || documentUrl || url;
},
requestCan(request, capability) {
return !this.isEnforced(request.tabId) || this.policy.can(request.url, capability, this.policyContext(request));
},
computeChildPolicy({url, contextUrl}, sender) {
let {tab, frameId} = sender;
let policy = ns.policy;
let {isTorBrowser} = ns.local;
if (!policy) {
console.log("Policy is null, initializing: %o, sending fallback.", ns.initializing);
return {
permissions: new Permissions(Permissions.DEFAULT).dry(),
unrestricted: false,
cascaded: false,
fallback: true,
isTorBrowser,
};
}
let tabId = tab ? tab.id : -1;
let topUrl;
if (frameId === 0) {
topUrl = url;
} else if (tab) {
if (!tab.url) tab = TabCache.get(tabId);
if (tab) topUrl = tab.url;
}
if (!topUrl) topUrl = url;
if (!contextUrl) contextUrl = topUrl;
if (Sites.isInternal(url) || !ns.isEnforced(tabId)) {
policy = null;
}
let permissions, unrestricted, cascaded;
if (policy) {
let perms = policy.get(url, contextUrl).perms;
cascaded = topUrl && ns.sync.cascadeRestrictions;
if (cascaded) {
perms = policy.cascadeRestrictions(perms, topUrl);
}
permissions = perms.dry();
} else {
// otherwise either internal URL or unrestricted
permissions = new Permissions(Permissions.ALL).dry();
unrestricted = true;
cascaded = false;
}
return {permissions, unrestricted, cascaded, isTorBrowser};
},
start() {
if (this.running) return;
this.running = true;
browser.runtime.onSyncMessage.addListener(onSyncMessage);
deferWebTraffic(this.initializing = init(),
async () => {
Commands.install();
try {
this.devMode = (await browser.management.getSelf()).installType === "development";
} catch(e) {}
if (!(this.local.debug || this.devMode)) {
debug = () => {}; // suppress verbosity
}
});
},
stop() {
if (!this.running) return;
this.running = false;
browser.runtime.onSyncMessage.removeListener(onSyncMessage);
Messages.removeHandler(messageHandler);
RequestGuard.stop();
log("STOPPED");
},
test() {
include("/test/run.js");
},
async testIC(callbackOrUrl) {
await include("xss/InjectionChecker.js");
let IC = await XSS.InjectionChecker;
let ic = new IC();
ic.logEnabled = true;
return (typeof callbackOrUrl === "function")
? await callbackOrUrl(ic)
: ic.checkUrl(callbackOrUrl);
},
async savePolicy() {
if (this.policy) {
await Storage.set("sync", {
policy: this.policy.dry()
});
await browser.webRequest.handlerBehaviorChanged()
}
return this.policy;
},
openOptionsPage({tab, focus, hilite}) {
let url = new URL(browser.runtime.getManifest().options_ui.page);
if (tab !== undefined) {
url.hash += `;tab-main-tabs=${tab}`;
}
let search = new URLSearchParams(url.search);
if (focus) search.set("focus", focus);
if (hilite) search.set("hilite", hilite);
url.search = search;
browser.tabs.create({url: url.toString() });
},
async save(obj) {
if (obj && obj.storage) {
let toBeSaved = {
[obj.storage]: obj
};
await Storage.set(obj.storage, toBeSaved);
}
return obj;
},
async collectSeen(tabId) {
try {
let seen = Array.from(await Messages.send("collect", {uuid: ns.local.uuid}, {tabId, frameId: 0}));
debug("Collected seen", seen);
return seen;
} catch (e) {
await include("/nscl/common/restricted.js");
if (!isRestrictedURL((await browser.tabs.get(tabId)).url)) {
// probably a page where content scripts cannot run, let's open the options instead
error(e, "Cannot collect noscript activity data");
}
}
return null;
},
};
}
ns.start();

View file

@ -0,0 +1,39 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
browser.runtime.onConnect.addListener(port => {
if (port.name === "noscript.popup") {
ns.popupOpened = true;
let pendingReload = false;
let tabId = -1;
port.onMessage.addListener(m => {
if ("pendingReload" in m) {
tabId = m.tabId;
pendingReload = m.pendingReload;
}
});
port.onDisconnect.addListener(() => {
ns.popupOpened = false;
if (pendingReload) {
browser.tabs.reload(tabId);
}
});
}
});