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/uuid.js
// depends on /nscl/service/Scripting.js
"use strict";
@ -56,7 +57,7 @@ var LifeCycle = (() => {
async createAndStore() {
let allSeen = {};
let tab;
await Promise.all((await browser.tabs.query({})).map(
await Promise.allSettled((await browser.tabs.query({})).map(
async t => {
let seen = await ns.collectSeen(t.id);
if (seen) {
@ -215,7 +216,7 @@ var LifeCycle = (() => {
destroyIfNeeded();
if (ns.initializing) await ns.initializing;
ns.policy = new Policy(policy);
await Promise.all(
await Promise.allSettled(
Object.entries(allSeen).map(
async ([tabId, seen]) => {
try {
@ -240,19 +241,19 @@ var LifeCycle = (() => {
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 =>
const 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;
await Promise.allSettled((await browser.tabs.query({})).map(async tab => {
try {
await Scripting.executeScript({
target: {tabId: tab.id, allFrames: true},
files: contentScripts,
});
} catch (e) {
await include("/nscl/common/restricted.js");
if (!isRestrictedURL(tab.url)) {
error(e, "Can't run content script on tab", tab);
}
}
}));

View File

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

View File

@ -18,13 +18,12 @@
* 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);
browser.action.setTitle({title: VERSION_LABEL});
const CSP_MARKER = "report-to noscript-reports";
const csp = new ReportingCSP(CSP_MARKER);
const policyTypesMap = {
main_frame: "",
@ -49,6 +48,32 @@ var RequestGuard = (() => {
}
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(),
_originsCache: new Map(),
types: ["script", "object", "media", "frame", "font"],
@ -89,6 +114,7 @@ var RequestGuard = (() => {
initTab(tabId, records = this.newRecords()) {
if (tabId < 0) return;
this.map.set(tabId, records);
this._session.save();
return records;
},
_record(request, what, optValue) {
@ -121,6 +147,7 @@ var RequestGuard = (() => {
collection[type] = [requestKey];
}
}
this._session.save();
return records;
},
record(request, what, optValue) {
@ -132,10 +159,11 @@ var RequestGuard = (() => {
}
},
_pendingTabs: new Set(),
updateTab(tabId) {
if (tabId < 0) return;
async updateTab(tabId) {
tabId ??= (await browser.tabs.getCurrent())?.tabId;
if (!(tabId >= 0)) return;
if (this._pendingTabs.size === 0) {
window.setTimeout(() => { // clamp UI updates
setTimeout(() => { // clamp UI updates
for (let tabId of this._pendingTabs) {
this._updateTabNow(tabId);
}
@ -166,22 +194,22 @@ var RequestGuard = (() => {
: (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})`});
let {action} = browser;
if (!action.setIcon) { // Fennec
action.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`}});
action.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}});
})();
browserAction.setBadgeText({
action.setBadgeText({
tabId,
text: TabGuard.isAnonymizedTab(tabId) ? "TG" : showBadge ? numBlocked.toString() : ""
});
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
browserAction.setTitle({tabId,
action.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
action.setTitle({tabId,
title: UA.mobile ? "NoScript" : `${VERSION_LABEL} \n${enforced ?
_("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}`
: _("NotEnforced")}`
@ -233,16 +261,15 @@ var RequestGuard = (() => {
TabStatus._originsCache.clear();
TabStatus._pendingTabs.delete(tabId);
},
}
for (let event of ["Activated", "Updated", "Removed"]) {
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
}
};
TabStatus.init();
const messageHandler = {
let messageHandler = {
async pageshow(message, sender) {
if (sender.frameId === 0) {
TabStatus.recordAll(sender.tab.id, message.seen);
} else {
} else if (sender.tab) {
// merge subframes records back into main frame's seen report
const tabId = sender.tab.id;
for (const {request, allowed, policyType} of message.seen) {
@ -253,20 +280,40 @@ var RequestGuard = (() => {
}
return true;
},
violation({url, type}, sender) {
let tabId = sender.tab.id;
let {frameId} = sender;
let r = {
url, type, tabId, frameId, documentUrl: sender.url
// returns true if it's a true violation (request should be blocked)
violation({url, type, isReport}, sender) {
const {tab, frameId} = sender;
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
if (type === "script" && url === sender.url) {
TabStatus.record(r, "noscriptFrame", true);
} else {
TabStatus.record(r, "blocked");
debug("CSP", isReport ? "report" : "violation", request, sender); // DEV_ONLY
if (isReport && !checkRequest(request)?.cancel) {
// not a real violation
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) {
let {url, documentUrl, policyType} = message;
let TAG = `<${policyType.toUpperCase()}>`;
@ -313,7 +360,8 @@ var RequestGuard = (() => {
}
return {enable: key};
},
}
};
const Content = {
async reportTo(request, allowed, policyType) {
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
@ -378,9 +426,9 @@ var RequestGuard = (() => {
function fakeOriginFromTab({tabId, type} = request) {
if (type !== "main_frame") {
let tab = tabId !== -1 && TabCache.get(tabId);
if (tab) {
return request.initiator = request.originUrl = request.documentUrl = tab.url;
let tabUrl = request.tabUrl || tabId !== -1 && TabCache.get(tabId)?.url;
if (tabUrl) {
return request.initiator = request.originUrl = request.documentUrl = tabUrl;
}
}
return request.initiator || request.originUrl;
@ -411,14 +459,11 @@ var RequestGuard = (() => {
};
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 (request.frameId !== 0 && ns.sync.cascadeRestrictions) {
const {tabUrl, frameAncestors} = request;
const topUrl = tabUrl ||
frameAncestors && frameAncestors[frameAncestors?.length - 1]?.url ||
TabCache.get(request.tabId)?.url;
if (topUrl) {
return ns.policy.cascadeRestrictions(perms, topUrl).capabilities;
}
@ -426,7 +471,9 @@ var RequestGuard = (() => {
return perms.capabilities;
}
const ABORT = {cancel: true}, ALLOW = {};
const ABORT = {cancel: true},
ALLOW = {};
const recent = {
MAX_AGE: 500,
_pendingGC: 0,
@ -448,6 +495,7 @@ var RequestGuard = (() => {
return null;
},
add(request) {
request.timeStamp ??= Date.now();
let last = this._byUrl.get(request.url);
if (!last) {
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 = {
onBeforeRequest(request) {
try {
if (browser.runtime.onSyncMessage && browser.runtime.onSyncMessage.isMessageRequest(request)) return ALLOW;
normalizeRequest(request);
if (browser.runtime?.onSyncMessage.isMessageRequest(request)) return ALLOW;
initPendingRequest(request);
let {policy} = ns
let {tabId, type, url, originUrl} = request;
let result = checkRequest(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) {
error(e);
}
@ -695,7 +758,7 @@ var RequestGuard = (() => {
promises = promises.filter(p => p instanceof Promise);
if (promises.length > 0) {
return Promise.all(promises).then(() => result);
return Promise.allSettled(promises).then(() => result);
}
return result;
@ -708,6 +771,9 @@ var RequestGuard = (() => {
TabStatus.initTab(tabId);
TabGuard.onCleanup(request);
}
if (!RequestGuard.canBlock) {
return;
}
let scriptBlocked = request.responseHeaders.some(
h => csp.isMine(h) && csp.blocks(h.value, "script")
);
@ -715,7 +781,6 @@ var RequestGuard = (() => {
TabStatus.record(request, "noscriptFrame", scriptBlocked);
let pending = pendingRequests.get(requestId);
if (pending) {
pending.scriptBlocked = scriptBlocked;
if (!(pending.headersProcessed &&
(scriptBlocked || ns.requestCan(request, "script"))
@ -746,81 +811,64 @@ var RequestGuard = (() => {
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) {
let {url, tabId, frameId} = details;
let policy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
policy.navigationURL = url;
const {url, tabId, frameId} = details;
const domPolicy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
domPolicy.navigationURL = url;
const callback = "ns_setupCallback";
if (DocStartInjection.mv3Callbacks) {
return {
data: {domPolicy},
callback,
assign: "ns",
};
}
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);
} ;
const domPolicy = ${JSON.stringify(domPolicy)};
if (globalThis.${callback}) {
globalThis.${callback}(domPolicy);
} else {
window.ns = {domPolicy}
globalThis.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", "requestHeaders"]);
// external interface
globalThis.RequestGuard = {
canBlock: UA.isMozilla,
DNRPolicy: null,
policyTypesMap,
};
let mergingCSP = "getBrowserInfo" in browser.runtime;
if (mergingCSP) {
let {vendor, version} = await browser.runtime.getBrowserInfo();
mergingCSP = vendor === "Mozilla" && parseInt(version) >= 77;
}
// initialization
{
Messages.addHandler(messageHandler);
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) {
// In Gecko>=77 (https://bugzilla.mozilla.org/show_bug.cgi?id=1462989)
// we need to cleanup our own cached headers in a dedicated listener :(
@ -848,33 +896,6 @@ var RequestGuard = (() => {
}
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

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

View File

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

View File

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

View File

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

View File

@ -126,21 +126,29 @@ var notifyPage = async () => {
window.addEventListener("pageshow", notifyPage);
let violations = new Set();
let documentOrigin = new URL(document.URL).origin;
window.addEventListener("securitypolicyviolation", e => {
const violations = new Set();
const documentOrigin = new URL(document.URL).origin;
window.addEventListener("securitypolicyviolation", async e => {
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 url = e.blockedURI;
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;
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
return;
}
let documentUrl = document.URL;
let origin;
if (!(url && url.includes(":"))) {
@ -149,23 +157,55 @@ window.addEventListener("securitypolicyviolation", e => {
} else {
({origin} = new URL(url));
}
let key = RequestKey.create(origin, type, documentOrigin);
const key = RequestKey.create(url, type, documentOrigin);
if (violations.has(key)) return;
violations.add(key);
if (type === "frame") type = "sub_frame";
seen.record({
policyType: type,
request: {
key,
url,
type,
documentUrl,
},
allowed: false
});
Messages.send("violation", {url, type});
Messages.send("violation", {url, type, isReport});
}, 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", () => {
seen.record({

View File

@ -172,6 +172,11 @@
return this.capabilities && this.capabilities.has(cap);
},
};
window.ns = window.ns ? Object.assign(ns, window.ns) : ns;
debug("StaticNS", Date.now(), JSON.stringify(window.ns)); // DEV_ONLY
globalThis.ns = globalThis.ns ? Object.assign(ns, globalThis.ns) : ns;
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__",
"incognito": "spanning",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'none'",
"icons": {
"48": "img/icon48.png",
"96": "img/icon96.png",
@ -27,15 +25,20 @@
"storage",
"tabs",
"unlimitedStorage",
"scripting",
"declarativeNetRequest",
"webNavigation",
"webRequest",
"webRequestBlocking",
"dns",
"<all_urls>"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"persistent": false,
"service_worker": "sw.js",
"scripts": [
"/nscl/lib/browser-polyfill.js",
"/nscl/lib/punycode.js",
@ -61,6 +64,7 @@
"/nscl/common/AddressMatcherWithDNS.js",
"/nscl/common/iputil.js",
"/nscl/common/SessionCache.js",
"/nscl/service/Scripting.js",
"/nscl/service/DocStartInjection.js",
"/nscl/service/LastListener.js",
"/nscl/service/patchWorkers.js",
@ -75,7 +79,8 @@
"bg/Settings.js",
"bg/main.js",
"common/themes.js"
]
],
"persistent": false
},
"content_scripts": [
@ -119,7 +124,6 @@
"match_origin_as_fallback": true,
"all_frames": true,
"js": [
"/nscl/common/UA.js",
"content/ftp.js",
"/nscl/content/DocumentFreezer.js",
"content/syncFetchPolicy.js"
@ -132,6 +136,14 @@
"open_in_tab": true
},
"action": {
"default_area": "navbar",
"default_title": "NoScript",
"default_icon": {
"64": "img/ui-maybe64.png"
}
},
"browser_action": {
"default_area": "navbar",
"default_title": "NoScript",
@ -141,12 +153,6 @@
},
"commands": {
"openPageUI": {
"description": "__MSG_pagePermissionsUI__",
"suggested_key": {
"default": "Alt+Shift+N"
}
},
"toggleEnforcementForTab": {
"description": "__MSG_toggleEnforcementForTab__",
"suggested_key": {
@ -154,6 +160,14 @@
"windows": "Alt+Shift+Comma"
}
},
"openPageUI": {
"description": "__MSG_pagePermissionsUI__",
"suggested_key": {
"default": "Alt+Shift+N"
}
},
"tempTrustPage": {
"description": "__MSG_TempTrustPage__"
},
@ -161,6 +175,7 @@
"description": "__MSG_RevokeTemp__"
},
"_execute_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("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()));
addEventListener("NoScriptThemeChanged", ({detail}) => {
if ("theme" in detail) {

View File

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

View File

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