api.js (10787B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* global ExtensionCommon, ExtensionAPI, Glean, Services, XPCOMUtils, ExtensionUtils */ 8 9 /* eslint-disable no-console */ 10 11 var { ExtensionParent } = ChromeUtils.importESModule( 12 "resource://gre/modules/ExtensionParent.sys.mjs" 13 ); 14 15 const { ChannelWrapper } = Cu.getGlobalForObject(ExtensionParent); 16 17 const lazy = {}; 18 19 ChromeUtils.defineESModuleGetters(lazy, { 20 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", 21 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 22 }); 23 24 const DATA_LEAK_BLOCKER_RS_COLLECTION = "addons-data-leak-blocker-domains"; 25 26 XPCOMUtils.defineLazyPreferenceGetter( 27 lazy, 28 "TEST_DOMAINS", 29 "extensions.data-leak-blocker@mozilla.com.testDomains", 30 "", 31 null, 32 value => { 33 try { 34 return new Set( 35 value 36 .split(",") 37 // Trim whitespaces. 38 .map(v => v.trim()) 39 // Omit any empty entries. 40 .filter(el => el) 41 ); 42 } catch { 43 // Return an empty set if parsing the value fails. 44 return new Set(); 45 } 46 } 47 ); 48 49 XPCOMUtils.defineLazyPreferenceGetter( 50 lazy, 51 "TESTING", 52 "extensions.data-leak-blocker@mozilla.com.testing", 53 false 54 ); 55 56 XPCOMUtils.defineLazyPreferenceGetter( 57 lazy, 58 "GLEAN_SUBMIT_TASK_TIMEOUT", 59 "extensions.data-leak-blocker@mozilla.com.gleanSubmitTaskTimeout", 60 5000 61 ); 62 63 this.dataAbuseDetection = class extends ExtensionAPI { 64 deferredPingSubmitTask = null; 65 domainsSet = null; 66 fogMetricsInitialized = false; 67 requestObserverRegistered = false; 68 rsClient = null; 69 rsSyncListener = null; 70 71 get IsShuttingDown() { 72 return ( 73 !this.extension || 74 this.extension.hasShutdown || 75 Services.startup.shuttingDown 76 ); 77 } 78 79 onStartup() { 80 this.deferredPingSubmitTask = new lazy.DeferredTask( 81 () => this.submitPing(), 82 lazy.GLEAN_SUBMIT_TASK_TIMEOUT 83 ); 84 85 ExtensionParent.browserStartupPromise 86 .then(() => { 87 if (this.IsShuttingDown) { 88 console.log( 89 "Data Leak Blocker initialization cancelled on detected extension or app shutdown" 90 ); 91 return Promise.resolve(); 92 } 93 this.registerFOGMetricsAndPings(); 94 this.rsClient = lazy.RemoteSettings(DATA_LEAK_BLOCKER_RS_COLLECTION); 95 // Process existing RS entries. 96 return this.onRemoteSettingsSync(); 97 }) 98 .then(() => { 99 if (this.IsShuttingDown) { 100 console.log( 101 "Data Leak Blocker initialization cancelled on detected extension or app shutdown" 102 ); 103 return; 104 } 105 Services.obs.addObserver(this, "http-on-modify-request"); 106 this.requestObserverRegistered = true; 107 this.rsSyncListener = this.onRemoteSettingsSync.bind(this); 108 this.rsClient.on("sync", this.rsSyncListener); 109 // Submit any events that may be collected for this custom ping 110 // in a previous session if it wasn't already sent. 111 this.deferredPingSubmitTask.arm(); 112 }); 113 } 114 115 onShutdown(isAppShutdown) { 116 if (isAppShutdown) { 117 return; 118 } 119 120 if (this.requestObserverRegistered) { 121 Services.obs.removeObserver(this, "http-on-modify-request"); 122 } 123 124 if (this.rsSyncListener) { 125 this.rsClient?.off("sync", this.rsSyncListener); 126 this.rsSyncListener = null; 127 } 128 this.rsClient = null; 129 130 if (this.deferredPingSubmitTask) { 131 this.deferredPingSubmitTask.finalize(); 132 this.deferredPingSubmitTask = null; 133 } 134 } 135 136 async onRemoteSettingsSync() { 137 const entries = await this.rsClient 138 .get({ syncIfEmpty: false }) 139 .catch(err => { 140 console.error( 141 `Failure to process ${DATA_LEAK_BLOCKER_RS_COLLECTION} RemoteSettings`, 142 err 143 ); 144 return []; 145 }); 146 const domains = new Set(); 147 for (const entry of entries) { 148 if (!Array.isArray(entry.domains)) { 149 if (lazy.TESTING) { 150 console.debug( 151 "Ignoring invalid RemoteSettings entry ('domains' property invalid or missing)", 152 entry 153 ); 154 } 155 continue; 156 } 157 for (const domain of entry.domains) { 158 if (!domain) { 159 if (lazy.TESTING) { 160 console.debug( 161 `Ignoring unxpected empty domain in ${DATA_LEAK_BLOCKER_RS_COLLECTION} record`, 162 entry 163 ); 164 } 165 continue; 166 } 167 domains.add(domain); 168 } 169 } 170 this.domainsSet = domains; 171 if (lazy.TESTING) { 172 this.domainsSet = domains.union(lazy.TEST_DOMAINS); 173 console.debug( 174 "Data Leak Blocker DomainsSet updated", 175 Array.from(this.domainsSet) 176 ); 177 } 178 } 179 180 registerFOGMetricsAndPings() { 181 // Register the custom ping to Glean (if not already registered). 182 // 183 // NOTE: this should be kept in sync with the ping as defined in the 184 // pings.yaml. 185 if (!("dataLeakBlocker" in GleanPings)) { 186 const ping = { 187 name: "data-leak-blocker", 188 includeClientId: true, 189 sendIfEmpty: false, 190 preciseTimestamp: false, 191 includeInfoSections: true, 192 enabled: true, 193 schedulesPings: [], 194 reasonCodes: [], 195 followsCollectionEnabled: true, 196 uploaderCapabilities: [], 197 }; 198 Services.fog.registerRuntimePing( 199 ping.name, 200 ping.includeClientId, 201 ping.sendIfEmpty, 202 ping.preciseTimestamp, 203 ping.includeInfoSections, 204 ping.enabled, 205 ping.schedulesPings, 206 ping.reasonCodes, 207 ping.followsCollectionEnabled, 208 ping.uploaderCapabilities 209 ); 210 } 211 212 // Register the custom metric to Glean (if not already registered). 213 // 214 // NOTE: this should be kept in sync with the metric as defined in the 215 // metrics.yaml. 216 if (!Glean.dataLeakBlocker?.reportV1) { 217 const metric = { 218 category: "data_leak_blocker", 219 name: "report_v1", 220 type: "event", 221 lifetime: "ping", 222 pings: ["data-leak-blocker"], 223 disabled: false, 224 extraArgs: { 225 allowed_extra_keys: [ 226 "addon_id", 227 "blocked", 228 "content_policy_type", 229 "is_addon_triggering", 230 "is_addon_loading", 231 "is_content_script", 232 "method", 233 ], 234 }, 235 }; 236 Services.fog.registerRuntimeMetric( 237 metric.type, 238 metric.category, 239 metric.name, 240 metric.pings, 241 `"${metric.lifetime}"`, 242 metric.disabled, 243 JSON.stringify(metric.extraArgs) 244 ); 245 } 246 this.fogMetricsInitialized = true; 247 } 248 249 submitPing() { 250 // NOTE: optional chaining is used here because on artifacts builds the runtime-registered 251 // glean ping defined by the registerFOGMetricsAndPings method would be unregistered 252 // as a side-effect of the jogfile for the artifacts build metrics being loaded 253 // (See Bug 1983674). 254 GleanPings.dataLeakBlocker?.submit(); 255 } 256 257 observe(subject, _topic, _data) { 258 try { 259 if (!this.domainsSet || !Glean.dataLeakBlocker?.reportV1) { 260 return; 261 } 262 this.processHttpOnModifyRequest( 263 subject.QueryInterface(Ci.nsIHttpChannel) 264 ); 265 } catch (err) { 266 if (lazy.TESTING) { 267 console.error( 268 "Unexpected error on processing http-on-modify-request notification", 269 err 270 ); 271 } 272 } 273 } 274 275 processHttpOnModifyRequest(channel) { 276 // Ignore internal favicon request triggered by FaviconLoader.sys.mjs. 277 if ( 278 channel.loadInfo.internalContentPolicyType === 279 Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON 280 ) { 281 return; 282 } 283 284 const { URI } = channel; 285 286 if (!URI.schemeIs("http") && !URI.schemeIs("https")) { 287 return; 288 } 289 290 const { triggeringPrincipal, loadingPrincipal, externalContentPolicyType } = 291 channel.loadInfo; 292 293 // Ignore requests that are not attributed to add-ons. 294 if ( 295 !triggeringPrincipal.isAddonOrExpandedAddonPrincipal && 296 !loadingPrincipal?.isAddonOrExpandedAddonPrincipal 297 ) { 298 return; 299 } 300 301 if (!this.domainsSet.has(URI.host)) { 302 return; 303 } 304 305 // numeric nsContentPolicyType enum value. 306 let content_policy_type = externalContentPolicyType; 307 let is_addon_loading = false; 308 let is_addon_triggering = false; 309 let is_content_script = false; 310 let method = channel.requestMethod; 311 let addonPolicy; 312 if (triggeringPrincipal.isAddonOrExpandedAddonPrincipal) { 313 is_addon_triggering = true; 314 is_content_script = !!triggeringPrincipal.contentScriptAddonPolicy; 315 addonPolicy = 316 triggeringPrincipal.addonPolicy ?? 317 triggeringPrincipal.contentScriptAddonPolicy; 318 } else if (loadingPrincipal?.isAddonOrExpandedAddonPrincipal) { 319 // Look for an addon id on the loadingPrincipal as a fallback 320 // (if it is defined, e.g. it is not defined for request triggered 321 // when a user loads an url in a new tab). 322 is_addon_loading = true; 323 is_content_script = !!loadingPrincipal.contentScriptAddonPolicy; 324 addonPolicy = 325 loadingPrincipal.addonPolicy ?? 326 loadingPrincipal.contentScriptAddonPolicy; 327 } else { 328 // Bail out if we can't determine an addon id for the request. 329 return; 330 } 331 332 if (lazy.TESTING) { 333 console.debug("Detected request to suspicious domain", channel.name); 334 } 335 336 const channelWrapper = ChannelWrapper.get(channel); 337 channelWrapper.cancel( 338 Cr.NS_ERROR_ABORT, 339 Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST 340 ); 341 let properties = channel.QueryInterface(Ci.nsIWritablePropertyBag); 342 properties.setProperty("cancelledByExtension", this.extension.id); 343 344 const addon_id = addonPolicy?.id ?? "no-addon-id"; 345 346 // NOTE: optional chaining is used here because on artifacts builds the runtime-registered 347 // glean ping defined by the registerFOGMetricsAndPings method would be unregistered 348 // as a side-effect of the jogfile for the artifacts build metrics being loaded 349 // (See Bug 1983674). 350 Glean.dataLeakBlocker?.reportV1.record({ 351 addon_id, 352 is_addon_triggering, 353 is_addon_loading, 354 is_content_script, 355 method, 356 content_policy_type, 357 blocked: true, 358 }); 359 360 this.deferredPingSubmitTask.arm(); 361 362 if (lazy.TESTING) { 363 console.debug("Suspicious request details", { 364 name: channel.name, 365 addon_id, 366 is_addon_triggering, 367 is_addon_loading, 368 is_content_script, 369 method, 370 content_policy_type, 371 blocked: true, 372 }); 373 } 374 } 375 };