TorRequestWatch.sys.mjs (3401B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const log = console.createInstance({ 6 maxLogLevelPref: "browser.torRequestWatch.log_level", 7 prefix: "TorRequestWatch", 8 }); 9 10 /** 11 * This request observer blocks all the cross-site requests to *.tor.onion 12 * domains to prevent fingerprinting Onion alias mechanisms (or their lack). 13 */ 14 class RequestObserver { 15 static #topics = [ 16 "http-on-modify-request", 17 "http-on-examine-response", 18 "http-on-examine-cached-response", 19 "http-on-examine-merged-response", 20 ]; 21 #asObserver(addOrRemove) { 22 const action = Services.obs[`${addOrRemove}Observer`].bind(Services.obs); 23 for (const topic of RequestObserver.#topics) { 24 action(this, topic); 25 } 26 } 27 28 start() { 29 this.#asObserver("add"); 30 log.debug("Started"); 31 } 32 stop() { 33 this.#asObserver("remove"); 34 log.debug("Stopped"); 35 } 36 37 // nsIObserver implementation 38 observe(subject, topic) { 39 try { 40 let channel = ChannelWrapper.get( 41 subject.QueryInterface(Ci.nsIHttpChannel) 42 ); 43 switch (topic) { 44 case "http-on-modify-request": 45 this.onRequest(channel); 46 break; 47 case "http-on-examine-cached-response": 48 case "http-on-examine-merged-response": 49 channel.isCached = true; 50 // falls through 51 case "http-on-examine-response": 52 this.onResponse(channel); 53 break; 54 } 55 } catch (e) { 56 log.error(e); 57 } 58 } 59 60 onRequest(channel) { 61 if (this.shouldBlind(channel, channel.documentURL)) { 62 log.warn(`Blocking cross-site ${channel.finalURL} ${channel.type} load.`); 63 channel.cancel(Cr.NS_ERROR_ABORT); 64 } 65 } 66 onResponse(channel) { 67 if (!channel.documentURL && this.shouldBlind(channel, channel.originURL)) { 68 const COOP = "cross-origin-opener-policy"; 69 // we break window.opener references if needed to mitigate XS-Leaks 70 for (let h of channel.getResponseHeaders()) { 71 if (h.name.toLowerCase() === COOP && h.value === "same-origin") { 72 log.debug(`${COOP} is already same-origin, nothing to do.`); 73 return; 74 } 75 } 76 log.warn(`Blinding cross-site ${channel.finalURL} load.`); 77 channel.setResponseHeader(COOP, "same-origin-allow-popups"); 78 } 79 } 80 81 isCrossOrigin(url1, url2) { 82 const origin1 = URL.parse(url1)?.origin; 83 const origin2 = URL.parse(url2)?.origin; 84 85 if (!origin1 || !origin2) { 86 return true; 87 } 88 89 return origin1 !== origin2; 90 } 91 shouldBlindCrossOrigin(uri) { 92 try { 93 let { host } = uri; 94 if (host.endsWith(".onion")) { 95 const previousPart = host.slice(-10, -6); 96 return ( 97 previousPart && (previousPart === ".tor" || previousPart === ".bit") 98 ); 99 } 100 } catch (e) { 101 // no host 102 } 103 return false; 104 } 105 shouldBlind(channel, sourceURL) { 106 return ( 107 sourceURL && 108 this.shouldBlindCrossOrigin(channel.finalURI) && 109 this.isCrossOrigin(channel.finalURL, sourceURL) 110 ); 111 } 112 } 113 114 let observer; 115 export const TorRequestWatch = { 116 start() { 117 if (!observer) { 118 (observer = new RequestObserver()).start(); 119 } 120 }, 121 stop() { 122 if (observer) { 123 observer.stop(); 124 observer = null; 125 } 126 }, 127 };