MV3 compatibility

This commit is contained in:
hackademix 2024-11-10 01:19:27 +01:00
parent 501f2c96ce
commit 404d6030e7
No known key found for this signature in database
GPG Key ID: 231A83AFDA9C2434
14 changed files with 438 additions and 314 deletions

View File

@ -20,6 +20,7 @@
// depends on /nscl/common/sha256.js // depends on /nscl/common/sha256.js
// depends on /nscl/common/uuid.js // depends on /nscl/common/uuid.js
// depends on /nscl/service/Scripting.js
"use strict"; "use strict";
@ -56,7 +57,7 @@ var LifeCycle = (() => {
async createAndStore() { async createAndStore() {
let allSeen = {}; let allSeen = {};
let tab; let tab;
await Promise.all((await browser.tabs.query({})).map( await Promise.allSettled((await browser.tabs.query({})).map(
async t => { async t => {
let seen = await ns.collectSeen(t.id); let seen = await ns.collectSeen(t.id);
if (seen) { if (seen) {
@ -215,7 +216,7 @@ var LifeCycle = (() => {
destroyIfNeeded(); destroyIfNeeded();
if (ns.initializing) await ns.initializing; if (ns.initializing) await ns.initializing;
ns.policy = new Policy(policy); ns.policy = new Policy(policy);
await Promise.all( await Promise.allSettled(
Object.entries(allSeen).map( Object.entries(allSeen).map(
async ([tabId, seen]) => { async ([tabId, seen]) => {
try { try {
@ -240,19 +241,19 @@ var LifeCycle = (() => {
if (!UA.isMozilla) { if (!UA.isMozilla) {
// Chromium does not inject content scripts at startup automatically for already loaded pages, // Chromium does not inject content scripts at startup automatically for already loaded pages,
// let's hack it manually. // let's hack it manually.
let contentScripts = browser.runtime.getManifest().content_scripts.find(s => const contentScripts = browser.runtime.getManifest().content_scripts.find(s =>
s.js && s.matches.includes("<all_urls>") && s.all_frames && s.match_about_blank).js; 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 => { await Promise.allSettled((await browser.tabs.query({})).map(async tab => {
for (let file of contentScripts) { try {
try { await Scripting.executeScript({
await browser.tabs.executeScript(tab.id, {file, allFrames: true, matchAboutBlank: true}); target: {tabId: tab.id, allFrames: true},
} catch (e) { files: contentScripts,
await include("/nscl/common/restricted.js"); });
if (!isRestrictedURL(tab.url)) { } catch (e) {
error(e, "Can't run content script on tab", tab); await include("/nscl/common/restricted.js");
} if (!isRestrictedURL(tab.url)) {
break; error(e, "Can't run content script on tab", tab);
} }
} }
})); }));

View File

@ -20,17 +20,11 @@
"use strict"; "use strict";
function ReportingCSP(marker, reportURI = "") { function ReportingCSP(marker) {
const DOM_SUPPORTED = "SecurityPolicyViolationEvent" in window;
if (DOM_SUPPORTED) reportURI = "";
return Object.assign( return Object.assign(
new CapsCSP(new NetCSP( new CapsCSP(new NetCSP(marker)),
reportURI ? `report-uri ${reportURI}` : marker
)),
{ {
reportURI,
patchHeaders(responseHeaders, capabilities) { patchHeaders(responseHeaders, capabilities) {
let header = null; let header = null;
let blocker; let blocker;

View File

@ -18,13 +18,12 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
var RequestGuard = (() => { {
'use strict'; 'use strict';
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`; const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
browser.browserAction.setTitle({title: VERSION_LABEL}); browser.action.setTitle({title: VERSION_LABEL});
const CSP_REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/"; const CSP_MARKER = "report-to noscript-reports";
const CSP_MARKER = "noscript-marker"; const csp = new ReportingCSP(CSP_MARKER);
let csp = new ReportingCSP(CSP_MARKER, CSP_REPORT_URI);
const policyTypesMap = { const policyTypesMap = {
main_frame: "", main_frame: "",
@ -49,6 +48,32 @@ var RequestGuard = (() => {
} }
const TabStatus = { const TabStatus = {
_session: new SessionCache(
"RequestGuard.TabStatus",
{
afterLoad(data) {
if (data) {
TabStatus.map = new Map(data.map);
TabStatus._originsCache = new Map(data._originsCache);
}
},
beforeSave() { // beforeSave
return {
map: [...TabStatus.map],
_originsCache: [...TabStatus._originsCache],
};
},
}
),
init() {
for (const event of ["Activated", "Updated", "Removed"]) {
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
}
(async () => {
await TabStatus._session.load();
TabStatus.updateTab();
});
},
map: new Map(), map: new Map(),
_originsCache: new Map(), _originsCache: new Map(),
types: ["script", "object", "media", "frame", "font"], types: ["script", "object", "media", "frame", "font"],
@ -89,6 +114,7 @@ var RequestGuard = (() => {
initTab(tabId, records = this.newRecords()) { initTab(tabId, records = this.newRecords()) {
if (tabId < 0) return; if (tabId < 0) return;
this.map.set(tabId, records); this.map.set(tabId, records);
this._session.save();
return records; return records;
}, },
_record(request, what, optValue) { _record(request, what, optValue) {
@ -121,6 +147,7 @@ var RequestGuard = (() => {
collection[type] = [requestKey]; collection[type] = [requestKey];
} }
} }
this._session.save();
return records; return records;
}, },
record(request, what, optValue) { record(request, what, optValue) {
@ -132,10 +159,11 @@ var RequestGuard = (() => {
} }
}, },
_pendingTabs: new Set(), _pendingTabs: new Set(),
updateTab(tabId) { async updateTab(tabId) {
if (tabId < 0) return; tabId ??= (await browser.tabs.getCurrent())?.tabId;
if (!(tabId >= 0)) return;
if (this._pendingTabs.size === 0) { if (this._pendingTabs.size === 0) {
window.setTimeout(() => { // clamp UI updates setTimeout(() => { // clamp UI updates
for (let tabId of this._pendingTabs) { for (let tabId of this._pendingTabs) {
this._updateTabNow(tabId); this._updateTabNow(tabId);
} }
@ -166,22 +194,22 @@ var RequestGuard = (() => {
: (numAllowed ? "sub" : "no")) // not topAllowed : (numAllowed ? "sub" : "no")) // not topAllowed
: "global"; // not enforced : "global"; // not enforced
let showBadge = ns.local.showCountBadge && numBlocked > 0; let showBadge = ns.local.showCountBadge && numBlocked > 0;
let browserAction = browser.browserAction; let {action} = browser;
if (!browserAction.setIcon) { // Fennec if (!action.setIcon) { // Fennec
browserAction.setTitle({tabId, title: `NoScript (${numBlocked})`}); action.setTitle({tabId, title: `NoScript (${numBlocked})`});
return; return;
} }
(async () => { (async () => {
let iconPath = (await Themes.isVintage()) ? '/img/vintage' : '/img'; let iconPath = (await Themes.isVintage()) ? '/img/vintage' : '/img';
browserAction.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}}); action.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}});
})(); })();
browserAction.setBadgeText({ action.setBadgeText({
tabId, tabId,
text: TabGuard.isAnonymizedTab(tabId) ? "TG" : showBadge ? numBlocked.toString() : "" text: TabGuard.isAnonymizedTab(tabId) ? "TG" : showBadge ? numBlocked.toString() : ""
}); });
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]}); action.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
browserAction.setTitle({tabId, action.setTitle({tabId,
title: UA.mobile ? "NoScript" : `${VERSION_LABEL} \n${enforced ? title: UA.mobile ? "NoScript" : `${VERSION_LABEL} \n${enforced ?
_("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}` _("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}`
: _("NotEnforced")}` : _("NotEnforced")}`
@ -233,16 +261,15 @@ var RequestGuard = (() => {
TabStatus._originsCache.clear(); TabStatus._originsCache.clear();
TabStatus._pendingTabs.delete(tabId); TabStatus._pendingTabs.delete(tabId);
}, },
} };
for (let event of ["Activated", "Updated", "Removed"]) { TabStatus.init();
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
} const messageHandler = {
let messageHandler = {
async pageshow(message, sender) { async pageshow(message, sender) {
if (sender.frameId === 0) { if (sender.frameId === 0) {
TabStatus.recordAll(sender.tab.id, message.seen); TabStatus.recordAll(sender.tab.id, message.seen);
} else { } else if (sender.tab) {
// merge subframes records back into main frame's seen report // merge subframes records back into main frame's seen report
const tabId = sender.tab.id; const tabId = sender.tab.id;
for (const {request, allowed, policyType} of message.seen) { for (const {request, allowed, policyType} of message.seen) {
@ -253,20 +280,40 @@ var RequestGuard = (() => {
} }
return true; return true;
}, },
violation({url, type}, sender) {
let tabId = sender.tab.id; // returns true if it's a true violation (request should be blocked)
let {frameId} = sender; violation({url, type, isReport}, sender) {
let r = { const {tab, frameId} = sender;
url, type, tabId, frameId, documentUrl: sender.url const documentUrl = sender.url;
let request = {
url,
type,
tabId: tab.id,
tabUrl: tab.url,
frameId,
documentUrl,
originUrl: documentUrl,
}; };
Content.reportTo(r, false, policyTypesMap[type]);
debug("Violation", type, url, tabId, frameId); // DEV_ONLY debug("CSP", isReport ? "report" : "violation", request, sender); // DEV_ONLY
if (type === "script" && url === sender.url) {
TabStatus.record(r, "noscriptFrame", true); if (isReport && !checkRequest(request)?.cancel) {
} else { // not a real violation
TabStatus.record(r, "blocked"); return false;
} }
Content.reportTo(request, false, policyTypesMap[type]);
if (type === "script" && url === sender.url) {
TabStatus.record(request, "noscriptFrame", true);
} else {
TabStatus.record(request, "blocked");
}
return true;
}, },
async blockedObjects(message, sender) { async blockedObjects(message, sender) {
let {url, documentUrl, policyType} = message; let {url, documentUrl, policyType} = message;
let TAG = `<${policyType.toUpperCase()}>`; let TAG = `<${policyType.toUpperCase()}>`;
@ -313,7 +360,8 @@ var RequestGuard = (() => {
} }
return {enable: key}; return {enable: key};
}, },
} };
const Content = { const Content = {
async reportTo(request, allowed, policyType) { async reportTo(request, allowed, policyType) {
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request; let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
@ -378,9 +426,9 @@ var RequestGuard = (() => {
function fakeOriginFromTab({tabId, type} = request) { function fakeOriginFromTab({tabId, type} = request) {
if (type !== "main_frame") { if (type !== "main_frame") {
let tab = tabId !== -1 && TabCache.get(tabId); let tabUrl = request.tabUrl || tabId !== -1 && TabCache.get(tabId)?.url;
if (tab) { if (tabUrl) {
return request.initiator = request.originUrl = request.documentUrl = tab.url; return request.initiator = request.originUrl = request.documentUrl = tabUrl;
} }
} }
return request.initiator || request.originUrl; return request.initiator || request.originUrl;
@ -411,14 +459,11 @@ var RequestGuard = (() => {
}; };
function intersectCapabilities(perms, request) { function intersectCapabilities(perms, request) {
let {frameId, frameAncestors, tabId} = request; if (request.frameId !== 0 && ns.sync.cascadeRestrictions) {
if (frameId !== 0 && ns.sync.cascadeRestrictions) { const {tabUrl, frameAncestors} = request;
let topUrl = frameAncestors && frameAncestors.length const topUrl = tabUrl ||
&& frameAncestors[frameAncestors.length - 1].url; frameAncestors && frameAncestors[frameAncestors?.length - 1]?.url ||
if (!topUrl) { TabCache.get(request.tabId)?.url;
let tab = TabCache.get(tabId);
if (tab) topUrl = tab.url;
}
if (topUrl) { if (topUrl) {
return ns.policy.cascadeRestrictions(perms, topUrl).capabilities; return ns.policy.cascadeRestrictions(perms, topUrl).capabilities;
} }
@ -426,7 +471,9 @@ var RequestGuard = (() => {
return perms.capabilities; return perms.capabilities;
} }
const ABORT = {cancel: true}, ALLOW = {}; const ABORT = {cancel: true},
ALLOW = {};
const recent = { const recent = {
MAX_AGE: 500, MAX_AGE: 500,
_pendingGC: 0, _pendingGC: 0,
@ -448,6 +495,7 @@ var RequestGuard = (() => {
return null; return null;
}, },
add(request) { add(request) {
request.timeStamp ??= Date.now();
let last = this._byUrl.get(request.url); let last = this._byUrl.get(request.url);
if (!last) { if (!last) {
last = [request]; last = [request];
@ -511,86 +559,101 @@ var RequestGuard = (() => {
} }
} }
// returns null if request.type is unknown, otherwise either ALLOW, ABORT or a redirect response
function checkRequest(request) {
if (!request.type in policyTypesMap) {
return null;
}
normalizeRequest(request);
let {tabId, type, url, originUrl} = request;
const {policy} = ns
let previous = recent.find(request);
if (previous) {
debug("Rapid fire request", previous); // DEV_ONLY
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(`${policyType} must be blocked`, request);
TabStatus.record(request, "blocked");
return previous.return = ABORT;
}
return ALLOW;
}
const listeners = { const listeners = {
onBeforeRequest(request) { onBeforeRequest(request) {
try { try {
if (browser.runtime.onSyncMessage && browser.runtime.onSyncMessage.isMessageRequest(request)) return ALLOW; if (browser.runtime?.onSyncMessage.isMessageRequest(request)) return ALLOW;
normalizeRequest(request);
initPendingRequest(request); initPendingRequest(request);
let {policy} = ns let result = checkRequest(request);
let {tabId, type, url, originUrl} = request; if (result) return result;
if (type in policyTypesMap) {
let previous = recent.find(request);
if (previous) {
debug("Rapid fire request", previous); // DEV_ONLY
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) { } catch (e) {
error(e); error(e);
} }
@ -695,7 +758,7 @@ var RequestGuard = (() => {
promises = promises.filter(p => p instanceof Promise); promises = promises.filter(p => p instanceof Promise);
if (promises.length > 0) { if (promises.length > 0) {
return Promise.all(promises).then(() => result); return Promise.allSettled(promises).then(() => result);
} }
return result; return result;
@ -708,6 +771,9 @@ var RequestGuard = (() => {
TabStatus.initTab(tabId); TabStatus.initTab(tabId);
TabGuard.onCleanup(request); TabGuard.onCleanup(request);
} }
if (!RequestGuard.canBlock) {
return;
}
let scriptBlocked = request.responseHeaders.some( let scriptBlocked = request.responseHeaders.some(
h => csp.isMine(h) && csp.blocks(h.value, "script") h => csp.isMine(h) && csp.blocks(h.value, "script")
); );
@ -715,7 +781,6 @@ var RequestGuard = (() => {
TabStatus.record(request, "noscriptFrame", scriptBlocked); TabStatus.record(request, "noscriptFrame", scriptBlocked);
let pending = pendingRequests.get(requestId); let pending = pendingRequests.get(requestId);
if (pending) { if (pending) {
pending.scriptBlocked = scriptBlocked; pending.scriptBlocked = scriptBlocked;
if (!(pending.headersProcessed && if (!(pending.headersProcessed &&
(scriptBlocked || ns.requestCan(request, "script")) (scriptBlocked || ns.requestCan(request, "script"))
@ -746,81 +811,64 @@ var RequestGuard = (() => {
TabGuard.onCleanup(request); TabGuard.onCleanup(request);
} }
}; };
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); // DEV_ONLY
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) { function injectPolicyScript(details) {
let {url, tabId, frameId} = details; const {url, tabId, frameId} = details;
let policy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId}); const domPolicy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
policy.navigationURL = url; domPolicy.navigationURL = url;
const callback = "ns_setupCallback";
if (DocStartInjection.mv3Callbacks) {
return {
data: {domPolicy},
callback,
assign: "ns",
};
}
let debugStatement = ns.local.debug ? ` let debugStatement = ns.local.debug ? `
let mark = Date.now() + ":" + Math.random(); let mark = Date.now() + ":" + Math.random();
console.debug("domPolicy", domPolicy, document.readyState, location.href, mark, window.ns);` : ''; console.debug("domPolicy", domPolicy, document.readyState, location.href, mark, window.ns);` : '';
return ` return `
let domPolicy = ${JSON.stringify(policy)}; const domPolicy = ${JSON.stringify(domPolicy)};
let {ns} = window; if (globalThis.${callback}) {
if (ns) { globalThis.${callback}(domPolicy);
ns.domPolicy = domPolicy;
if (ns.setup) {
if (ns.syncSetup) ns.syncSetup(domPolicy);
else ns.setup(domPolicy);
} ;
} else { } else {
window.ns = {domPolicy} globalThis.ns ||= {domPolicy}
} }
${debugStatement}`; ${debugStatement}`;
} }
const RequestGuard = { // external interface
async start() { globalThis.RequestGuard = {
Messages.addHandler(messageHandler); canBlock: UA.isMozilla,
let wr = browser.webRequest; DNRPolicy: null,
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args); policyTypesMap,
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", "requestHeaders"]);
let mergingCSP = "getBrowserInfo" in browser.runtime; // initialization
if (mergingCSP) { {
let {vendor, version} = await browser.runtime.getBrowserInfo(); Messages.addHandler(messageHandler);
mergingCSP = vendor === "Mozilla" && parseInt(version) >= 77; const wr = browser.webRequest;
} const listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
const allUrls = ["<all_urls>"];
const docTypes = ["main_frame", "sub_frame", "object"];
const filterDocs = {urls: allUrls, types: docTypes};
const filterAll = {urls: allUrls};
listen("onBeforeRequest", filterAll,
RequestGuard.canBlock ? ["blocking"] : []);
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
listen("onCompleted", filterAll);
listen("onErrorOccurred", filterAll);
DocStartInjection.register(injectPolicyScript);
TabStatus.probe();
if (!RequestGuard.canBlock) {
include("/bg/DNRPolicy.js")
} else {
// From here on, only webRequestBlocking-enabled code (Gecko MV2.5)
listen("onBeforeSendHeaders", filterAll, ["blocking", "requestHeaders"]);
const mergingCSP = true; // TODO: check whether it's still true...
if (mergingCSP) { if (mergingCSP) {
// In Gecko>=77 (https://bugzilla.mozilla.org/show_bug.cgi?id=1462989) // In Gecko>=77 (https://bugzilla.mozilla.org/show_bug.cgi?id=1462989)
// we need to cleanup our own cached headers in a dedicated listener :( // we need to cleanup our own cached headers in a dedicated listener :(
@ -848,33 +896,6 @@ var RequestGuard = (() => {
} }
return ALLOW; return ALLOW;
}, filterDocs, ["blocking", "responseHeaders"])).install(); }, 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

@ -163,7 +163,7 @@ var Settings = {
reloadOptionsUI = true; reloadOptionsUI = true;
} }
await Promise.all(["local", "sync"].map( await Promise.allSettled(["local", "sync"].map(
async storage => (settings[storage] || // changed or... async storage => (settings[storage] || // changed or...
settings[storage] === null // ... needs reset to default settings[storage] === null // ... needs reset to default
) && await ns.save(settings[storage] ) && await ns.save(settings[storage]
@ -180,7 +180,7 @@ var Settings = {
} }
if (typeof unrestrictedTab === "boolean") { if (typeof unrestrictedTab === "boolean") {
ns.toggleTabRestrictions(tabId, !unrestrictedTab); await ns.toggleTabRestrictions(tabId, !unrestrictedTab);
} }
if (reloadAffected && tabId !== -1) { if (reloadAffected && tabId !== -1) {
try { try {

View File

@ -18,6 +18,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// depends on /nscl/service/Scripting.js
// depends on /nscl/common/SessionCache.js // depends on /nscl/common/SessionCache.js
// depends on /nscl/service/TabCache.js // depends on /nscl/service/TabCache.js
// depends on /nscl/service/TabTies.js // depends on /nscl/service/TabTies.js
@ -111,6 +112,7 @@ var TabGuard = (() => {
wakening: Promise.all([TabCache.wakening, TabTies.wakening, session.load()]), wakening: Promise.all([TabCache.wakening, TabTies.wakening, session.load()]),
forget, forget,
// must be called from a webRequest.onBeforeSendHeaders blocking listener // must be called from a webRequest.onBeforeSendHeaders blocking listener
// TODO: explore DNR alternative
onSend(request) { onSend(request) {
const mode = ns.sync.TabGuardMode; const mode = ns.sync.TabGuardMode;
if (mode === "off" || !request.incognito && mode!== "global") return; if (mode === "off" || !request.incognito && mode!== "global") return;
@ -189,16 +191,18 @@ var TabGuard = (() => {
return suspiciousTabs.length > 0 && (async () => { return suspiciousTabs.length > 0 && (async () => {
let suspiciousDomains = []; let suspiciousDomains = [];
await Promise.all(suspiciousTabs.map(async (tab) => { await Promise.allSettled(suspiciousTabs.map(async (tab) => {
if (!tab._isExplicitOrigin) { // e.g. about:blank if (!tab._isExplicitOrigin) { // e.g. about:blank
// let's try retrieving actual origin // let's try retrieving actual origin
tab._externalUrl = tab.url; tab._externalUrl = tab.url;
tab._isExplicitOrigin = true; tab._isExplicitOrigin = true;
try { try {
tab.url = await browser.tabs.executeScript(tab.id, { tab.url = (await Scripting.executeScript({
runAt: "document_start", target: {tabId: tab.id, allFrames: false},
code: "window.origin === 'null' ? window.location.href : window.origin" func: () => {
}); return window.origin === 'null' ? window.location.href : window.origin;
},
}))[0].result;
} catch (e) { } catch (e) {
// We don't have permissions to run in this tab, probably because it has been left empty. // We don't have permissions to run in this tab, probably because it has been left empty.
debug(e); debug(e);
@ -223,10 +227,10 @@ var TabGuard = (() => {
} }
if (!tab._contentType) { if (!tab._contentType) {
try { try {
tab._contentType = await browser.tabs.executeScript(tab.id, { tab._contentType = (await Scripting.executeScript({
runAt: "document_start", target: {tabId: tab.id},
code: "document.contentType" func() { return document.contentType }
}); }))[0].result;
} catch (e) { } catch (e) {
// We don't have permissions to run in this tab: privileged! // We don't have permissions to run in this tab: privileged!
debug(e); debug(e);
@ -292,6 +296,7 @@ var TabGuard = (() => {
})(); })();
}, },
// must be called from a webRequest.onHeadersReceived blocking listener // must be called from a webRequest.onHeadersReceived blocking listener
// TODO: explore DNR alternative
onReceive(request) { onReceive(request) {
if (!anonymizedRequests.has(request.id)) return false; if (!anonymizedRequests.has(request.id)) return false;
let headersModified = false; let headersModified = false;

View File

@ -18,7 +18,9 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
{ // depends on /nscl/service/Scripting.js
{
'use strict'; 'use strict';
{ {
for (let event of ["onInstalled", "onUpdateAvailable"]) { for (let event of ["onInstalled", "onUpdateAvailable"]) {
@ -36,7 +38,7 @@
const menuUpdater = async (tabId) => { const menuUpdater = async (tabId) => {
if (!menuShowing) return; if (!menuShowing) return;
try { try {
const badgeText = await browser.browserAction.getBadgeText({tabId}); const badgeText = await browser.action.getBadgeText({tabId});
let title = "NoScript"; let title = "NoScript";
if (badgeText) title = `${title} [${badgeText}]`; if (badgeText) title = `${title} [${badgeText}]`;
await browser.contextMenus.update(ctxMenuId, {title}); await browser.contextMenus.update(ctxMenuId, {title});
@ -69,9 +71,9 @@
{ {
afterLoad(data) { afterLoad(data) {
if (data) { if (data) {
ns.policy = new Policy(data.policy);
ns.unrestrictedTabs = new Set(data.unrestrictedTabs);
ns.gotTorBrowserInit = data.gotTorBrowserInit; ns.gotTorBrowserInit = data.gotTorBrowserInit;
ns.unrestrictedTabs = new Set(data.unrestrictedTabs);
ns.policy = new Policy(data.policy);
} }
}, },
beforeSave() { // beforeSave beforeSave() { // beforeSave
@ -90,10 +92,12 @@
if (!ns.policy) { // ns.policy could have been already set by LifeCycle or SessionCache if (!ns.policy) { // ns.policy could have been already set by LifeCycle or SessionCache
const policyData = (await Storage.get("sync", "policy")).policy; const policyData = (await Storage.get("sync", "policy")).policy;
if (policyData && policyData.DEFAULT) { if (policyData && policyData.DEFAULT) {
ns.policy = new Policy(policyData); const policy = new Policy(policyData);
if (ns.local.enforceOnRestart && !ns.policy.enforced) { if (ns.local.enforceOnRestart && !policy.enforced) {
ns.policy.enforced = true; (ns.policy = policy).enforced = true;
await ns.savePolicy(); await ns.savePolicy();
} else {
ns.policy = policy;
} }
} else { } else {
ns.policy = new Policy(Settings.createDefaultDryPolicy()); ns.policy = new Policy(Settings.createDefaultDryPolicy());
@ -108,7 +112,6 @@
await include("/nscl/service/prefetchCSSResources.js"); await include("/nscl/service/prefetchCSSResources.js");
} }
await TabGuard.wakening; await TabGuard.wakening;
await RequestGuard.start();
try { try {
await Messages.send("started"); await Messages.send("started");
@ -130,7 +133,7 @@
active: true active: true
})); }));
if (tab) { if (tab) {
ns.toggleTabRestrictions(tab.id); await ns.toggleTabRestrictions(tab.id);
browser.tabs.reload(tab.id); browser.tabs.reload(tab.id);
} }
}, },
@ -154,7 +157,7 @@
} }
// wiring main UI // wiring main UI
browser.browserAction.setPopup({popup: popupURL}); browser.action.setPopup({popup: popupURL});
} }
}; };
@ -222,29 +225,30 @@
}); });
}, },
async getTheme(msg, {tab, frameId}) { async getTheme(msg, {tab, frameId}) {
let code = await Themes.getContentCSS(); let css = await Themes.getContentCSS();
if (!ns.local.showProbePlaceholders) { if (!ns.local.showProbePlaceholders) {
code += `\n.__NoScript_Offscreen_PlaceHolders__ {display: none}`; css += `\n.__NoScript_Offscreen_PlaceHolders__ {display: none}`;
} }
try { try {
browser.tabs.insertCSS(tab.id, { await Scripting.insertCSS({
code, target: {tabId: tab.id, frameId},
frameId, css,
runAt: "document_start",
matchAboutBlank: true,
cssOrigin: "user",
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
return {"vintage": await Themes.isVintage()}; const ret = {"vintage": await Themes.isVintage()};
console.debug("Returning from getTheme", ret); // DEV_ONLY
return ret;
}, },
async promptHook(msg, {tabId}) { async promptHook(msg, {tabId}) {
await browser.tabs.executeScript(tabId, { const func = () => {
code: "try { if (document.fullscreenElement) document.exitFullscreen(); } catch (e) {}", try { if (document.fullscreenElement) document.exitFullscreen(); } catch (e) {}
matchAboutBlank: true, };
allFrames: true, await Scripting.executeScript({
target: {tabId},
func,
}); });
}, },
@ -265,16 +269,24 @@
} }
} }
var ns = { let _policy = null;
globalThis.ns = {
running: false, running: false,
policy: null, set policy(p) {
_policy = p;
RequestGuard.DNRPolicy?.update();
},
get policy() { return _policy; },
local: null, local: null,
sync: null, sync: null,
initializing: null, initializing: null,
unrestrictedTabs: new Set(), unrestrictedTabs: new Set(),
toggleTabRestrictions(tabId, restrict = ns.unrestrictedTabs.has(tabId)) { async toggleTabRestrictions(tabId, restrict = ns.unrestrictedTabs.has(tabId)) {
ns.unrestrictedTabs[restrict ? "delete": "add"](tabId); ns.unrestrictedTabs[restrict ? "delete": "add"](tabId);
session.save(); Promise.allSettled([session.save(),
RequestGuard.DNRPolicy?.updateTabs()
]);
}, },
isEnforced(tabId = -1) { isEnforced(tabId = -1) {
return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId)); return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId));
@ -342,7 +354,8 @@
async init() { async init() {
browser.runtime.onSyncMessage.addListener(onSyncMessage); browser.runtime.onSyncMessage.addListener(onSyncMessage);
await Wakening.waitFor(Messages.wakening = this.initializing = init()); await (Messages.wakening = this.initializing = init());
Wakening.done();
Commands.install(); Commands.install();
try { try {
this.devMode = (await browser.management.getSelf()).installType === "development"; this.devMode = (await browser.management.getSelf()).installType === "development";
@ -369,7 +382,7 @@
async savePolicy() { async savePolicy() {
if (this.policy) { if (this.policy) {
await Promise.all([ await Promise.allSettled([
Storage.set("sync", { Storage.set("sync", {
policy: this.policy.dry() policy: this.policy.dry()
}), }),

View File

@ -28,6 +28,10 @@ a.__NoScript_PlaceHolder__ {
border-radius: 4px; border-radius: 4px;
} }
a.__NoScript_PlaceHolder__.no-theme {
visibility: hidden !important;
}
a.__NoScript_PlaceHolder__.mozilla { a.__NoScript_PlaceHolder__.mozilla {
background-image: var(--img-logo); background-image: var(--img-logo);
} }

View File

@ -126,21 +126,29 @@ var notifyPage = async () => {
window.addEventListener("pageshow", notifyPage); window.addEventListener("pageshow", notifyPage);
let violations = new Set(); const violations = new Set();
let documentOrigin = new URL(document.URL).origin; const documentOrigin = new URL(document.URL).origin;
window.addEventListener("securitypolicyviolation", e => {
window.addEventListener("securitypolicyviolation", async e => {
if (!e.isTrusted) return; if (!e.isTrusted) return;
let {violatedDirective, originalPolicy} = e; let {violatedDirective, originalPolicy, disposition} = e;
let type = violatedDirective.split("-", 1)[0]; // e.g. script-src 'none' => script let type = violatedDirective.split("-", 1)[0]; // e.g. script-src 'none' => script
let url = e.blockedURI; let url = e.blockedURI;
if (type === "media" && /^data\b/.test(url) && (!CSP.isMediaBlocker(originalPolicy) || if (type === "media" && /^data\b/.test(url) && (!CSP.isMediaBlocker(originalPolicy) ||
ns.embeddingDocument || !document.querySelector("video,audio"))) ns.embeddingDocument || !document.querySelector("video,audio"))) {
// MediaBlocker probe, don't report
return; return;
if (!ns.CSP || !(CSP.normalize(originalPolicy).includes(ns.CSP))) { }
const isReport = disposition === "report" &&
/; report-to noscript-reports-[\w-]+$/.test(originalPolicy);
if (!(isReport ||
ns.CSP && CSP.normalize(originalPolicy).includes(ns.CSP))) {
// this seems to come from page's own CSP // this seems to come from page's own CSP
return; return;
} }
let documentUrl = document.URL; let documentUrl = document.URL;
let origin; let origin;
if (!(url && url.includes(":"))) { if (!(url && url.includes(":"))) {
@ -149,23 +157,55 @@ window.addEventListener("securitypolicyviolation", e => {
} else { } else {
({origin} = new URL(url)); ({origin} = new URL(url));
} }
let key = RequestKey.create(origin, type, documentOrigin); const key = RequestKey.create(url, type, documentOrigin);
if (violations.has(key)) return; if (violations.has(key)) return;
violations.add(key); violations.add(key);
if (type === "frame") type = "sub_frame"; if (type === "frame") type = "sub_frame";
seen.record({ Messages.send("violation", {url, type, isReport});
policyType: type,
request: {
key,
url,
type,
documentUrl,
},
allowed: false
});
Messages.send("violation", {url, type});
}, true); }, true);
if (!/^https:/.test(location.protocol)) {
// Reporting CSP can only be injected in HTTP responses,
// let's emulate them using mutation observers
const checked = new Set();
const checkSrc = async (node) => {
if (!('src' in node && node.parentNode)) {
return;
}
const type = node instanceof HTMLMediaElement ? "media"
: node instanceof HTMLIFrameElement ? "sub_frame"
: node instanceof HTMLObjectElement || node instanceof HTMLEmbedElement ? "object"
: "";
if (!type) {
return;
}
const url = node.src;
const key = RequestKey.create(url, type, documentOrigin);
if (checked.has(key)) {
return;
}
checked.add(key);
Messages.send("violation", {url, type, isReport: true});
}
const mutationsCallback = records => {
for (var r of records) {
switch (r.type) {
case "attributes":
checkSrc(r.target);
break;
case "childList":
[...r.addedNodes].forEach(checkSrc);
break;
}
}
};
const observer = new MutationObserver(mutationsCallback);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributeFilter: ["src"],
});
}
ns.on("capabilities", () => { ns.on("capabilities", () => {
seen.record({ seen.record({

View File

@ -172,6 +172,11 @@
return this.capabilities && this.capabilities.has(cap); return this.capabilities && this.capabilities.has(cap);
}, },
}; };
window.ns = window.ns ? Object.assign(ns, window.ns) : ns; globalThis.ns = globalThis.ns ? Object.assign(ns, globalThis.ns) : ns;
debug("StaticNS", Date.now(), JSON.stringify(window.ns)); // DEV_ONLY debug("StaticNS", Date.now(), JSON.stringify(globalThis.ns)); // DEV_ONLY
globalThis.ns_setupCallBack = ns.domPolicy
? () => {}
: ({domPolicy}) => {
};
} }

View File

@ -14,8 +14,6 @@
"description": "__MSG_Description__", "description": "__MSG_Description__",
"incognito": "spanning", "incognito": "spanning",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'none'",
"icons": { "icons": {
"48": "img/icon48.png", "48": "img/icon48.png",
"96": "img/icon96.png", "96": "img/icon96.png",
@ -27,15 +25,20 @@
"storage", "storage",
"tabs", "tabs",
"unlimitedStorage", "unlimitedStorage",
"scripting",
"declarativeNetRequest",
"webNavigation", "webNavigation",
"webRequest", "webRequest",
"webRequestBlocking", "webRequestBlocking",
"dns", "dns",
"<all_urls>" "<all_urls>"
], ],
"host_permissions": [
"<all_urls>"
],
"background": { "background": {
"persistent": false, "service_worker": "sw.js",
"scripts": [ "scripts": [
"/nscl/lib/browser-polyfill.js", "/nscl/lib/browser-polyfill.js",
"/nscl/lib/punycode.js", "/nscl/lib/punycode.js",
@ -61,6 +64,7 @@
"/nscl/common/AddressMatcherWithDNS.js", "/nscl/common/AddressMatcherWithDNS.js",
"/nscl/common/iputil.js", "/nscl/common/iputil.js",
"/nscl/common/SessionCache.js", "/nscl/common/SessionCache.js",
"/nscl/service/Scripting.js",
"/nscl/service/DocStartInjection.js", "/nscl/service/DocStartInjection.js",
"/nscl/service/LastListener.js", "/nscl/service/LastListener.js",
"/nscl/service/patchWorkers.js", "/nscl/service/patchWorkers.js",
@ -75,7 +79,8 @@
"bg/Settings.js", "bg/Settings.js",
"bg/main.js", "bg/main.js",
"common/themes.js" "common/themes.js"
] ],
"persistent": false
}, },
"content_scripts": [ "content_scripts": [
@ -119,7 +124,6 @@
"match_origin_as_fallback": true, "match_origin_as_fallback": true,
"all_frames": true, "all_frames": true,
"js": [ "js": [
"/nscl/common/UA.js",
"content/ftp.js", "content/ftp.js",
"/nscl/content/DocumentFreezer.js", "/nscl/content/DocumentFreezer.js",
"content/syncFetchPolicy.js" "content/syncFetchPolicy.js"
@ -132,6 +136,14 @@
"open_in_tab": true "open_in_tab": true
}, },
"action": {
"default_area": "navbar",
"default_title": "NoScript",
"default_icon": {
"64": "img/ui-maybe64.png"
}
},
"browser_action": { "browser_action": {
"default_area": "navbar", "default_area": "navbar",
"default_title": "NoScript", "default_title": "NoScript",
@ -141,12 +153,6 @@
}, },
"commands": { "commands": {
"openPageUI": {
"description": "__MSG_pagePermissionsUI__",
"suggested_key": {
"default": "Alt+Shift+N"
}
},
"toggleEnforcementForTab": { "toggleEnforcementForTab": {
"description": "__MSG_toggleEnforcementForTab__", "description": "__MSG_toggleEnforcementForTab__",
"suggested_key": { "suggested_key": {
@ -154,6 +160,14 @@
"windows": "Alt+Shift+Comma" "windows": "Alt+Shift+Comma"
} }
}, },
"openPageUI": {
"description": "__MSG_pagePermissionsUI__",
"suggested_key": {
"default": "Alt+Shift+N"
}
},
"tempTrustPage": { "tempTrustPage": {
"description": "__MSG_TempTrustPage__" "description": "__MSG_TempTrustPage__"
}, },
@ -161,6 +175,7 @@
"description": "__MSG_RevokeTemp__" "description": "__MSG_RevokeTemp__"
}, },
"_execute_action": {},
"_execute_browser_action": {} "_execute_browser_action": {}
} }
} }

23
src/sw.js Normal file
View File

@ -0,0 +1,23 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2024 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/>.
*/
importScripts("/nscl/common/include.js");
browser.browserAction = browser.action;
console.log("NoScript Manifest V3 service worker");

View File

@ -156,7 +156,7 @@ document.querySelector("#version").textContent = _("Version",
opt("showFullAddresses", "local"); opt("showFullAddresses", "local");
opt("showProbePlaceholders", "local"); opt("showProbePlaceholders", "local");
UI.wireChoice("theme", o => Themes.setup(o && o.value) ); UI.wireChoice("theme", async o => await Themes.setup(o?.value) );
opt("vintageTheme", async o => await (o ? Themes.setVintage(o.checked) : Themes.isVintage())); opt("vintageTheme", async o => await (o ? Themes.setVintage(o.checked) : Themes.isVintage()));
addEventListener("NoScriptThemeChanged", ({detail}) => { addEventListener("NoScriptThemeChanged", ({detail}) => {
if ("theme" in detail) { if ("theme" in detail) {

View File

@ -249,13 +249,16 @@ addEventListener("unload", e => {
setupEnforcement(); setupEnforcement();
let mainFrame = UI.seen && UI.seen.find(thing => thing.request.type === "main_frame"); let mainFrame = UI.seen && UI.seen.find(thing => thing.request.type === "main_frame");
debug("Seen: %o", UI.seen); debug("Seen: %o", UI.seen);
if (!mainFrame) { if (!mainFrame) {
let isHttp = /^https?:/.test(pageTab.url); let isHttp = /^https?:/.test(pageTab.url);
try { try {
await browser.tabs.executeScript(tabId, { code: "" }); await include("/nscl/service/Scripting.js");
await Scripting.executeScript({
target: {tabId, allFrames: false},
func: () => {}
});
if (isHttp) { if (isHttp) {
document.body.classList.add("disabled"); document.body.classList.add("disabled");
messageBox("warning", _("freshInstallReload")); messageBox("warning", _("freshInstallReload"));

View File

@ -39,12 +39,12 @@ var XSS = (() => {
async function getUserResponse(xssReq) { async function getUserResponse(xssReq) {
let {originKey, request} = xssReq; let {originKey, request} = xssReq;
let {tabId, frameId} = request; let {tabId, frameId} = request;
let {browserAction} = browser; const {action} = browser;
if (frameId === 0) { if (frameId === 0) {
if (blockedTabs.has(tabId)) { if (blockedTabs.has(tabId)) {
blockedTabs.delete(tabId); blockedTabs.delete(tabId);
if ("setBadgeText" in browserAction) { if ("setBadgeText" in action) {
browserAction.setBadgeText({tabId, text: ""}); action.setBadgeText({tabId, text: ""});
} }
} }
} }
@ -57,9 +57,9 @@ var XSS = (() => {
log("Blocking request from %s to %s by previous XSS prompt user choice", log("Blocking request from %s to %s by previous XSS prompt user choice",
xssReq.srcUrl, xssReq.destUrl); xssReq.srcUrl, xssReq.destUrl);
if ("setBadgeText" in browserAction) { if ("setBadgeText" in action) {
browserAction.setBadgeText({tabId, text: "XSS"}); action.setBadgeText({tabId, text: "XSS"});
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]}); action.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
} }
let keys = blockedTabs.get(tabId); let keys = blockedTabs.get(tabId);
if (!keys) blockedTabs.set(tabId, keys = new Set()); if (!keys) blockedTabs.set(tabId, keys = new Set());