Skip to content

Commit dd4042f

Browse files
committed
Cross-tab identity leak protection (tor-browser#41071)
1 parent c052ea2 commit dd4042f

9 files changed

Lines changed: 169 additions & 4 deletions

File tree

src/_locales/en/messages.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,5 +706,33 @@
706706
},
707707
"DonateLong": {
708708
"message": "NoScript is Free Software and can't exist without your help. Please donate now!"
709+
},
710+
711+
"TabGuard_label": {
712+
"message": "Cross-tab identity leak protection"
713+
},
714+
"TabGuard_optEnabled": {
715+
"message": "Enabled everywhere"
716+
},
717+
"TabGuard_optIncognito": {
718+
"message": "Enabled in Private Browsing only"
719+
},
720+
"TabGuard_optDisabled": {
721+
"message": "Disabled"
722+
},
723+
"TabGuard_forget": {
724+
"message": "Forget decisions."
725+
},
726+
"TabGuard_title": {
727+
"message": "Potential Identity Leak"
728+
},
729+
"TabGuard_message": {
730+
"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."
731+
},
732+
"TabGuard_optAnonymize": {
733+
"message": "Load anonymously"
734+
},
735+
"TabGuard_optAllow": {
736+
"message": "Load normally"
709737
}
710738
}

src/bg/Defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var Defaults = {
3333
sync: {
3434
global: false,
3535
xss: true,
36+
TabGuardMode: "incognito",
3637
cascadeRestrictions : false,
3738
overrideTorBrowserPolicy: false, // note: Settings.update() on reset will flip this to true
3839
}

src/bg/RequestGuard.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,10 +550,16 @@ var RequestGuard = (() => {
550550
}
551551
return ALLOW;
552552
},
553+
553554
onBeforeSendHeaders(request) {
554555
normalizeRequest(request);
555-
return checkLANRequest(request);
556+
let lanRes = checkLANRequest(request);
557+
if (!UA.isMozilla) return lanRes; // Chromium doesn't support async blocking suspension, stop here
558+
if (lanRes === ABORT) return ABORT;
559+
let chainNext = r => r === ABORT ? r : TabGuard.check(request);
560+
return lanRes instanceof Promise ? lanRes.then(chainNext) : chainNext(lanRes);
556561
},
562+
557563
onHeadersReceived(request) {
558564
// called for main_frame, sub_frame and object
559565

@@ -754,7 +760,7 @@ var RequestGuard = (() => {
754760
let filterDocs = {urls: allUrls, types: docTypes};
755761
let filterAll = {urls: allUrls};
756762
listen("onBeforeRequest", filterAll, ["blocking"]);
757-
listen("onBeforeSendHeaders", filterAll, ["blocking"]);
763+
listen("onBeforeSendHeaders", filterAll, ["blocking", "requestHeaders"]);
758764

759765
let mergingCSP = "getBrowserInfo" in browser.runtime;
760766
if (mergingCSP) {

src/bg/TabGuard.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
3+
*
4+
* Copyright (C) 2005-2022 Giorgio Maone <https://maone.net>
5+
*
6+
* SPDX-License-Identifier: GPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License as published by the Free Software
10+
* Foundation, either version 3 of the License, or (at your option) any later
11+
* version.
12+
*
13+
* This program is distributed in the hope that it will be useful, but WITHOUT
14+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15+
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License along with
18+
* this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
22+
var TabGuard = (() => {
23+
(async () => { await include(["/nscl/service/TabCache.js", "/nscl/service/TabTies.js"]); })();
24+
25+
let allowedGroups, filteredGroups;
26+
let forget = () => {
27+
allowedGroups = {};
28+
filteredGroups = {};
29+
};
30+
forget();
31+
32+
const AUTH_HEADERS_RX = /^(?:authorization|cookie)/i;
33+
34+
function getDomain(u) {
35+
let {url} = Sites.parse(u);
36+
return url && url.protocol.startsWith("http") && tld.getDomain(url.hostname);
37+
}
38+
39+
return {
40+
forget,
41+
check(request) {
42+
const mode = ns.sync.TabGuardMode;
43+
if (mode === "off" || !request.incognito && mode!== "global") return;
44+
45+
const {tabId, type, url} = request;
46+
47+
if (tabId < 0) return; // no tab, no party
48+
49+
let targetDomain = getDomain(url);
50+
if (!targetDomain) return; // no domain, no cookies
51+
52+
const mainFrame = type === "main_frame";
53+
let tabDomain = getDomain(mainFrame ? url : TabCache.get(tabId).url);
54+
if (!tabDomain) return; // no domain, no cookies
55+
56+
let ties = TabTies.get(tabId);
57+
if (ties.size === 0) return; // no ties, no party
58+
59+
let legitDomains = allowedGroups[tabDomain] || new Set([tabDomain]);
60+
61+
let otherDomains = new Set([...ties].map(id => getDomain(TabCache.get(id).url)).filter(d => !legitDomains.has(d)));
62+
if (otherDomains.size === 0) return; // no cross-site ties, no party
63+
64+
let {requestHeaders} = request;
65+
66+
if (!requestHeaders.some(h => AUTH_HEADERS_RX.test(h.name))) return; // no auth, no party
67+
68+
// danger zone
69+
70+
let filterAuth = () => {
71+
requestHeaders = requestHeaders.filter(h => !AUTH_HEADERS_RX.test(h.name));
72+
debug("TabGuard removing auth headers from %o (%o)", request, requestHeaders);
73+
return {requestHeaders};
74+
};
75+
76+
if (mainFrame) {
77+
let quietDomains = filteredGroups[tabDomain];
78+
let mustPrompt = (!quietDomains || [...otherDomains].some(d => !quietDomains.has(d)));
79+
if (mustPrompt) {
80+
return (async () => {
81+
let options = [
82+
{label: _("TabGuard_optAnonymize"), checked: true},
83+
{label: _("TabGuard_optAllow")},
84+
];
85+
let ret = await Prompts.prompt({
86+
title: _("TabGuard_title"),
87+
message: _("TabGuard_message", [tabDomain, [...otherDomains].join(", ")]),
88+
options});
89+
if (ret.button === 1) return {cancel: true};
90+
let list = ret.option === 0 ? filteredGroups : allowedGroups;
91+
otherDomains.add(tabDomain);
92+
for (let d of otherDomains) list[d] = otherDomains;
93+
return list === filteredGroups ? filterAuth() : null;
94+
})();
95+
}
96+
}
97+
98+
return filterAuth();
99+
100+
}
101+
}
102+
})();

src/bg/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@
156156

157157
let messageHandler = {
158158
async updateSettings(settings, sender) {
159+
if (settings.command === "tg-forget") {
160+
TabGuard.forget();
161+
delete settings.tabGuardCommand;
162+
}
159163
await Settings.update(settings);
160164
toggleCtxMenuItem();
161165
},

src/nscl

src/ui/options.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ <h3 class="flextabs__tab"><button class="flextabs__toggle">__MSG_SectionAdvanced
167167
</span>
168168
</div>
169169

170+
<section id="sect-TabGuard">
171+
<fieldset id="TabGuard">
172+
<legend>__MSG_TabGuard_label__</legend>
173+
<div class="opt-group">
174+
<span id="tgMode">
175+
<input id="tgm-global" type="radio" name="TabGuardMode" value="global" /><label for="tgm-global">__MSG_TabGuard_optEnabled__</label>
176+
<input id="tgm-incognito" type="radio" name="TabGuardMode" value="incognito" checked="checked"/><label for="tgm-incognito">__MSG_TabGuard_optIncognito__</label>
177+
<input id="tgm-off" type="radio" name="TabGuardMode" value="off"/><label for="tgm-off">__MSG_TabGuard_optDisabled__</label>
178+
</span>
179+
<div id="tgForget"><button id="tgForgetButton">__MSG_TabGuard_forget__</button></div>
180+
</div>
181+
182+
183+
</fieldset>
184+
</section>
185+
170186
<section id="debug" class="browser-style">
171187
<div class="opt-group">
172188
<span><input type="checkbox" id="opt-debug"><label id="label-debug" for="opt-debug">Debug</label></span>

src/ui/options.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ document.querySelector("#version").textContent = _("Version",
141141
if (checked) updateRawPolicyEditor();
142142
});
143143

144+
UI.wireChoice("TabGuardMode");
145+
146+
document.querySelector("#tgForgetButton").onclick = e => {
147+
e.target.disabled = true;
148+
UI.updateSettings({command: "tg-forget"});
149+
};
150+
144151
// Appearance
145152

146153
opt("showCountBadge", "local");

src/ui/ui.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ var UI = (() => {
9292
async pullSettings() {
9393
Messages.send("broadcastSettings", {tabId: UI.tabId});
9494
},
95-
async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected}) {
95+
async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected, command}) {
9696
if (policy) policy = policy.dry(true);
9797
return await Messages.send("updateSettings", {
9898
policy,
@@ -102,6 +102,7 @@ var UI = (() => {
102102
sync,
103103
reloadAffected,
104104
tabId: UI.tabId,
105+
command
105106
});
106107
},
107108

0 commit comments

Comments
 (0)