Cross-tab identity leak protection (tor-browser#41071)

This commit is contained in:
hackademix 2022-08-10 01:05:33 +02:00
parent c052ea2a18
commit dd4042f10f
9 changed files with 169 additions and 4 deletions

View File

@ -706,5 +706,33 @@
},
"DonateLong": {
"message": "NoScript is Free Software and can't exist without your help. Please donate now!"
},
"TabGuard_label": {
"message": "Cross-tab identity leak protection"
},
"TabGuard_optEnabled": {
"message": "Enabled everywhere"
},
"TabGuard_optIncognito": {
"message": "Enabled in Private Browsing only"
},
"TabGuard_optDisabled": {
"message": "Disabled"
},
"TabGuard_forget": {
"message": "Forget decisions."
},
"TabGuard_title": {
"message": "Potential Identity Leak"
},
"TabGuard_message": {
"message": "You are about to load a page from $1.\nIf you are a $1 logged-in user, information about your identity might be acquired by $2."
},
"TabGuard_optAnonymize": {
"message": "Load anonymously"
},
"TabGuard_optAllow": {
"message": "Load normally"
}
}

View File

@ -33,6 +33,7 @@ var Defaults = {
sync: {
global: false,
xss: true,
TabGuardMode: "incognito",
cascadeRestrictions : false,
overrideTorBrowserPolicy: false, // note: Settings.update() on reset will flip this to true
}

View File

@ -550,10 +550,16 @@ var RequestGuard = (() => {
}
return ALLOW;
},
onBeforeSendHeaders(request) {
normalizeRequest(request);
return checkLANRequest(request);
let lanRes = checkLANRequest(request);
if (!UA.isMozilla) return lanRes; // Chromium doesn't support async blocking suspension, stop here
if (lanRes === ABORT) return ABORT;
let chainNext = r => r === ABORT ? r : TabGuard.check(request);
return lanRes instanceof Promise ? lanRes.then(chainNext) : chainNext(lanRes);
},
onHeadersReceived(request) {
// called for main_frame, sub_frame and object
@ -754,7 +760,7 @@ var RequestGuard = (() => {
let filterDocs = {urls: allUrls, types: docTypes};
let filterAll = {urls: allUrls};
listen("onBeforeRequest", filterAll, ["blocking"]);
listen("onBeforeSendHeaders", filterAll, ["blocking"]);
listen("onBeforeSendHeaders", filterAll, ["blocking", "requestHeaders"]);
let mergingCSP = "getBrowserInfo" in browser.runtime;
if (mergingCSP) {

102
src/bg/TabGuard.js Normal file
View File

@ -0,0 +1,102 @@
/*
* 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 TabGuard = (() => {
(async () => { await include(["/nscl/service/TabCache.js", "/nscl/service/TabTies.js"]); })();
let allowedGroups, filteredGroups;
let forget = () => {
allowedGroups = {};
filteredGroups = {};
};
forget();
const AUTH_HEADERS_RX = /^(?:authorization|cookie)/i;
function getDomain(u) {
let {url} = Sites.parse(u);
return url && url.protocol.startsWith("http") && tld.getDomain(url.hostname);
}
return {
forget,
check(request) {
const mode = ns.sync.TabGuardMode;
if (mode === "off" || !request.incognito && mode!== "global") return;
const {tabId, type, url} = request;
if (tabId < 0) return; // no tab, no party
let targetDomain = getDomain(url);
if (!targetDomain) return; // no domain, no cookies
const mainFrame = type === "main_frame";
let tabDomain = getDomain(mainFrame ? url : TabCache.get(tabId).url);
if (!tabDomain) return; // no domain, no cookies
let ties = TabTies.get(tabId);
if (ties.size === 0) return; // no ties, no party
let legitDomains = allowedGroups[tabDomain] || new Set([tabDomain]);
let otherDomains = new Set([...ties].map(id => getDomain(TabCache.get(id).url)).filter(d => !legitDomains.has(d)));
if (otherDomains.size === 0) return; // no cross-site ties, no party
let {requestHeaders} = request;
if (!requestHeaders.some(h => AUTH_HEADERS_RX.test(h.name))) return; // no auth, no party
// danger zone
let filterAuth = () => {
requestHeaders = requestHeaders.filter(h => !AUTH_HEADERS_RX.test(h.name));
debug("TabGuard removing auth headers from %o (%o)", request, requestHeaders);
return {requestHeaders};
};
if (mainFrame) {
let quietDomains = filteredGroups[tabDomain];
let mustPrompt = (!quietDomains || [...otherDomains].some(d => !quietDomains.has(d)));
if (mustPrompt) {
return (async () => {
let options = [
{label: _("TabGuard_optAnonymize"), checked: true},
{label: _("TabGuard_optAllow")},
];
let ret = await Prompts.prompt({
title: _("TabGuard_title"),
message: _("TabGuard_message", [tabDomain, [...otherDomains].join(", ")]),
options});
if (ret.button === 1) return {cancel: true};
let list = ret.option === 0 ? filteredGroups : allowedGroups;
otherDomains.add(tabDomain);
for (let d of otherDomains) list[d] = otherDomains;
return list === filteredGroups ? filterAuth() : null;
})();
}
}
return filterAuth();
}
}
})();

View File

@ -156,6 +156,10 @@
let messageHandler = {
async updateSettings(settings, sender) {
if (settings.command === "tg-forget") {
TabGuard.forget();
delete settings.tabGuardCommand;
}
await Settings.update(settings);
toggleCtxMenuItem();
},

@ -1 +1 @@
Subproject commit 1a222e51f8ea529e611ff6e51d5f135feebb54e3
Subproject commit 763457e53e56a4116dd7f37c7ca0da9572fc1dc1

View File

@ -167,6 +167,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
</span>
</div>
<section id="sect-TabGuard">
<fieldset id="TabGuard">
<legend>__MSG_TabGuard_label__</legend>
<div class="opt-group">
<span id="tgMode">
<input id="tgm-global" type="radio" name="TabGuardMode" value="global" /><label for="tgm-global">__MSG_TabGuard_optEnabled__</label>
<input id="tgm-incognito" type="radio" name="TabGuardMode" value="incognito" checked="checked"/><label for="tgm-incognito">__MSG_TabGuard_optIncognito__</label>
<input id="tgm-off" type="radio" name="TabGuardMode" value="off"/><label for="tgm-off">__MSG_TabGuard_optDisabled__</label>
</span>
<div id="tgForget"><button id="tgForgetButton">__MSG_TabGuard_forget__</button></div>
</div>
</fieldset>
</section>
<section id="debug" class="browser-style">
<div class="opt-group">
<span><input type="checkbox" id="opt-debug"><label id="label-debug" for="opt-debug">Debug</label></span>

View File

@ -141,6 +141,13 @@ document.querySelector("#version").textContent = _("Version",
if (checked) updateRawPolicyEditor();
});
UI.wireChoice("TabGuardMode");
document.querySelector("#tgForgetButton").onclick = e => {
e.target.disabled = true;
UI.updateSettings({command: "tg-forget"});
};
// Appearance
opt("showCountBadge", "local");

View File

@ -92,7 +92,7 @@ var UI = (() => {
async pullSettings() {
Messages.send("broadcastSettings", {tabId: UI.tabId});
},
async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected}) {
async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected, command}) {
if (policy) policy = policy.dry(true);
return await Messages.send("updateSettings", {
policy,
@ -102,6 +102,7 @@ var UI = (() => {
sync,
reloadAffected,
tabId: UI.tabId,
command
});
},