DeclarativeNetRequest-backed policy enforcement.
This commit is contained in:
parent
971697043c
commit
501f2c96ce
|
@ -0,0 +1,290 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
{
|
||||||
|
const DEFAULT_PRIORITY = 1;
|
||||||
|
const SITE_PRIORITY = 10;
|
||||||
|
const CTX_PRIORITY = 20;
|
||||||
|
const CASCADE_PRIORITY = 30;
|
||||||
|
const TAB_PRIORITY = 40;
|
||||||
|
const REPORT_PRIORITY = 50;
|
||||||
|
const MAX_PRIORITY = 100;
|
||||||
|
|
||||||
|
const SESSION_BASE = 100;
|
||||||
|
const TAB_BASE = 10000;
|
||||||
|
const DYNAMIC_BASE = 20000;
|
||||||
|
|
||||||
|
let _lastPolicy;
|
||||||
|
|
||||||
|
const resourceTypesMap = {};
|
||||||
|
{
|
||||||
|
const dnrTypes = Object.values(browser.declarativeNetRequest.ResourceType);
|
||||||
|
|
||||||
|
for(const [key, value] of Object.entries(RequestGuard.policyTypesMap)) {
|
||||||
|
if (!(value && dnrTypes.includes(key))) continue;
|
||||||
|
const mapping = resourceTypesMap[value] ||= [];
|
||||||
|
mapping.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceTypeFor = {
|
||||||
|
block(caps) {
|
||||||
|
return this.allow(Permissions.ALL.filter(cap => !caps.has(cap)));
|
||||||
|
},
|
||||||
|
allow(caps) {
|
||||||
|
const resourceTypes = [];
|
||||||
|
for (let c of [...caps]) {
|
||||||
|
if (c in resourceTypesMap) {
|
||||||
|
resourceTypes.push(...resourceTypesMap[c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resourceTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forBlockAllow(capabilities, callback) {
|
||||||
|
for (const actionType of ["block", "allow"]) {
|
||||||
|
const resourceTypes = ResourceTypeFor[actionType](capabilities);
|
||||||
|
if (resourceTypes?.length) {
|
||||||
|
callback(actionType, resourceTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUrlFilter(siteKey) {
|
||||||
|
let urlFilter = `|${siteKey.replace(/^§:/, '|')}`;
|
||||||
|
if (!urlFilter.replace(/^\w+:\/+/).includes("/")) {
|
||||||
|
urlFilter += "/";
|
||||||
|
}
|
||||||
|
return urlFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportedCaps = ['script', 'object', 'media', 'frame', 'font'];
|
||||||
|
const reportingCSP = `${reportedCaps
|
||||||
|
.map(cap => `${cap}-src 'none'`)
|
||||||
|
.join(';')
|
||||||
|
}; report-to noscript-reports-${uuid()}`; // see /content/content.js securitypolicyviolation handler
|
||||||
|
|
||||||
|
let updatingSemaphore;
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
await updatingSemaphore;
|
||||||
|
|
||||||
|
const {policy} = ns;
|
||||||
|
if (policy === _lastPolicy) {
|
||||||
|
if (!policy || policy.equals(_lastPolicy)) {
|
||||||
|
return await updateTabs();
|
||||||
|
}
|
||||||
|
_lastPolicy = policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rules = {
|
||||||
|
// Using capitalized keys to allow DRY tricks with get/update methods
|
||||||
|
Session: [],
|
||||||
|
Dynamic: [{
|
||||||
|
id: 1,
|
||||||
|
priority: REPORT_PRIORITY,
|
||||||
|
action: {
|
||||||
|
type: "modifyHeaders",
|
||||||
|
responseHeaders: [{
|
||||||
|
header: "content-security-policy-report-only",
|
||||||
|
operation: "set",
|
||||||
|
value: reportingCSP,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
resourceTypes: ["main_frame", "sub_frame"],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
lastId: 1,
|
||||||
|
add({capabilities, temp}, priority = SITE_PRIORITY, siteKey) {
|
||||||
|
const urlFilter = siteKey ? toUrlFilter(siteKey) : undefined;
|
||||||
|
forBlockAllow(capabilities, (type, resourceTypes) => {
|
||||||
|
const rules = temp ? this.Session : this.Dynamic;
|
||||||
|
const id = (temp ? SESSION_BASE : DYNAMIC_BASE) + rules.length;
|
||||||
|
rules.push({
|
||||||
|
id,
|
||||||
|
priority,
|
||||||
|
action: {
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
urlFilter,
|
||||||
|
resourceTypes,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (policy?.enforced) {
|
||||||
|
Rules.add(policy.DEFAULT, DEFAULT_PRIORITY);
|
||||||
|
for (const [siteKey, perms] of [...policy.sites]) {
|
||||||
|
Rules.add(perms, SITE_PRIORITY, siteKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await addTabRules(Rules.Session);
|
||||||
|
|
||||||
|
const ts = Date.now(); // DEV_ONLY
|
||||||
|
await Promise.allSettled(["Dynamic", "Session"].map((async (ruleType) => {
|
||||||
|
const ts = Date.now(); // DEV_ONLY
|
||||||
|
const removeRuleIds = (
|
||||||
|
await browser.declarativeNetRequest[`get${ruleType}Rules`]()
|
||||||
|
).filter(r => r.priority <= MAX_PRIORITY).map(r => r.id);
|
||||||
|
try {
|
||||||
|
await browser.declarativeNetRequest[`update${ruleType}Rules`]({
|
||||||
|
addRules: Rules[ruleType],
|
||||||
|
removeRuleIds,
|
||||||
|
});
|
||||||
|
console.debug(`DNRPolicy ${Rules[ruleType].length} ${ruleType} rules updated in ${Date.now() - ts}ms`); // DEV_ONLY
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, `Failed to update DNRPolicy ${ruleType}rules %o - remove %o, add %o`, Rules[ruleType], addRules, removeRuleIds);
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
console.debug(`All DNRPolicy rules updated in ${Date.now() - ts}ms`); // DEV_ONLY
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTabRules(rules = []) {
|
||||||
|
if (ns.unrestrictedTabs.size) {
|
||||||
|
rules.push({
|
||||||
|
id: TAB_BASE,
|
||||||
|
priority: TAB_PRIORITY,
|
||||||
|
action: {
|
||||||
|
type: "allowAllRequests",
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
tabIds: [...ns.unrestrictedTabs],
|
||||||
|
resourceTypes: ["main_frame", "sub_frame"],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await addCtxRules(rules);
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addCtxRules(rules) {
|
||||||
|
const {policy} = ns;
|
||||||
|
const cascade = ns.sync.cascadeRestrictions;
|
||||||
|
const ctxSettings = [...policy.sites].filter(([siteKey, perms]) => perms.contextual?.size);
|
||||||
|
const tabs = (ctxSettings.length || cascade) &&
|
||||||
|
(await browser.tabs.query({})).filter(tab => !ns.unrestrictedTabs.has(tab.id));
|
||||||
|
if (!tabs?.length) {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
for (const [siteKey, perms] of ctxSettings) {
|
||||||
|
const tabIds = tabs.filter(tab => perms.contextual.match(tab.url)).map(tab => tab.id);
|
||||||
|
if (!tabIds.length) continue;
|
||||||
|
const urlFilter = toUrlFilter(siteKey);
|
||||||
|
forBlockAllow(perms.capabilities, (type, resourceTypes) => {
|
||||||
|
rules.push({
|
||||||
|
id: TAB_BASE + rules.length,
|
||||||
|
priority: CTX_PRIORITY,
|
||||||
|
action: {
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
tabIds,
|
||||||
|
urlFilter,
|
||||||
|
resourceTypes,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!cascade) {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
const tabPresets = new Map();
|
||||||
|
for({url, id} of tabs) {
|
||||||
|
const resourceTypes = ResourceTypeFor.block(policy.get(url).perms.capabilities);
|
||||||
|
if (!resourceTypes.length) continue;
|
||||||
|
const key = JSON.stringify(resourceTypes);
|
||||||
|
if (tabPresets.has(key)) {
|
||||||
|
tabPresets.get(key).tabIds.push(id);
|
||||||
|
} else {
|
||||||
|
tabPresets.set(key, {
|
||||||
|
resourceTypes,
|
||||||
|
tabIds: [id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const {resourceTypes, tabIds} of tabPresets.values()) {
|
||||||
|
rules.push({
|
||||||
|
id: TAB_BASE + rules.length,
|
||||||
|
priority: CASCADE_PRIORITY,
|
||||||
|
action: {
|
||||||
|
type: "block",
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
tabIds,
|
||||||
|
resourceTypes,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTabs() {
|
||||||
|
const ts = Date.now();
|
||||||
|
const removeRuleIds = (
|
||||||
|
await browser.declarativeNetRequest.getSessionRules()
|
||||||
|
).filter(r => r.id >= TAB_BASE && r.id < DYNAMIC_BASE &&
|
||||||
|
r.priority <= MAX_PRIORITY && r.condition.tabIds)
|
||||||
|
.map(r => r.id);
|
||||||
|
const addRules = await addTabRules();
|
||||||
|
try {
|
||||||
|
await browser.declarativeNetRequest.updateSessionRules({
|
||||||
|
addRules,
|
||||||
|
removeRuleIds,
|
||||||
|
});
|
||||||
|
console.debug(`DNRPolicy tab-bound rules updated in ${Date.now() - ts}ms`); // DEV_ONLY
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, `Failed to update DNRPolicy tab-bound rules (remove %o, add %o)`, addRules, removeRuleIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestGuard.DNRPolicy = {
|
||||||
|
async update() {
|
||||||
|
await updatingSemaphore;
|
||||||
|
updatingSemaphore = await update();
|
||||||
|
},
|
||||||
|
async updateTabs() {
|
||||||
|
await updatingSemaphore;
|
||||||
|
updatingSemaphore = await updateTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||||
|
if (changeInfo.url) {
|
||||||
|
// TODO: see if the update can be made more granular
|
||||||
|
await RequestGuard.DNRPolicy.updateTabs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let delay;
|
||||||
|
browser.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
||||||
|
// let's coalesce tabs updates on close
|
||||||
|
delay ??= setTimeout(() => {
|
||||||
|
delay = undefined;
|
||||||
|
RequestGuard.DNRPolicy.updateTabs();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue