1/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6var EXPORTED_SYMBOLS = ["NetErrorParent"]; 7 8const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 9const { XPCOMUtils } = ChromeUtils.import( 10 "resource://gre/modules/XPCOMUtils.jsm" 11); 12const { PrivateBrowsingUtils } = ChromeUtils.import( 13 "resource://gre/modules/PrivateBrowsingUtils.jsm" 14); 15const { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm"); 16 17const { TelemetryController } = ChromeUtils.import( 18 "resource://gre/modules/TelemetryController.jsm" 19); 20 21const PREF_SSL_IMPACT_ROOTS = [ 22 "security.tls.version.", 23 "security.ssl3.", 24 "security.tls13.", 25]; 26 27ChromeUtils.defineModuleGetter( 28 this, 29 "BrowserUtils", 30 "resource://gre/modules/BrowserUtils.jsm" 31); 32 33XPCOMUtils.defineLazyServiceGetter( 34 this, 35 "gSerializationHelper", 36 "@mozilla.org/network/serialization-helper;1", 37 "nsISerializationHelper" 38); 39 40class CaptivePortalObserver { 41 constructor(actor) { 42 this.actor = actor; 43 Services.obs.addObserver(this, "captive-portal-login-abort"); 44 Services.obs.addObserver(this, "captive-portal-login-success"); 45 } 46 47 stop() { 48 Services.obs.removeObserver(this, "captive-portal-login-abort"); 49 Services.obs.removeObserver(this, "captive-portal-login-success"); 50 } 51 52 observe(aSubject, aTopic, aData) { 53 switch (aTopic) { 54 case "captive-portal-login-abort": 55 case "captive-portal-login-success": 56 // Send a message to the content when a captive portal is freed 57 // so that error pages can refresh themselves. 58 this.actor.sendAsyncMessage("AboutNetErrorCaptivePortalFreed"); 59 break; 60 } 61 } 62} 63 64class NetErrorParent extends JSWindowActorParent { 65 constructor() { 66 super(); 67 this.captivePortalObserver = new CaptivePortalObserver(this); 68 } 69 70 didDestroy() { 71 if (this.captivePortalObserver) { 72 this.captivePortalObserver.stop(); 73 } 74 } 75 76 get browser() { 77 return this.browsingContext.top.embedderElement; 78 } 79 80 getSecurityInfo(securityInfoAsString) { 81 if (!securityInfoAsString) { 82 return null; 83 } 84 85 let securityInfo = gSerializationHelper.deserializeObject( 86 securityInfoAsString 87 ); 88 securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); 89 90 return securityInfo; 91 } 92 93 hasChangedCertPrefs() { 94 let prefSSLImpact = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => { 95 return prefs.concat(Services.prefs.getChildList(root)); 96 }, []); 97 for (let prefName of prefSSLImpact) { 98 if (Services.prefs.prefHasUserValue(prefName)) { 99 return true; 100 } 101 } 102 103 return false; 104 } 105 106 async ReportBlockingError(bcID, scheme, host, port, path, xfoAndCspInfo) { 107 // For reporting X-Frame-Options error and CSP: frame-ancestors errors, We 108 // are collecting 4 pieces of information. 109 // 1. The X-Frame-Options in the response header. 110 // 2. The CSP: frame-ancestors in the response header. 111 // 3. The URI of the frame who triggers this error. 112 // 4. The top-level URI which loads the frame. 113 // 114 // We will exclude the query strings from the reporting URIs. 115 // 116 // More details about the data we send can be found in 117 // https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/xfocsp-error-report-ping.html 118 // 119 120 let topBC = BrowsingContext.get(bcID).top; 121 let topURI = topBC.currentWindowGlobal.documentURI; 122 123 // Get the URLs without query strings. 124 let frame_uri = `${scheme}://${host}${port == -1 ? "" : ":" + port}${path}`; 125 let top_uri = `${topURI.scheme}://${topURI.hostPort}${topURI.filePath}`; 126 127 TelemetryController.submitExternalPing( 128 "xfocsp-error-report", 129 { 130 ...xfoAndCspInfo, 131 frame_hostname: host, 132 top_hostname: topURI.host, 133 frame_uri, 134 top_uri, 135 }, 136 { addClientId: false, addEnvironment: false } 137 ); 138 } 139 140 /** 141 * Return the default start page for the cases when the user's own homepage is 142 * infected, so we can get them somewhere safe. 143 */ 144 getDefaultHomePage(win) { 145 if (PrivateBrowsingUtils.isWindowPrivate(win)) { 146 return win.BROWSER_NEW_TAB_URL; 147 } 148 let url = HomePage.getDefault(); 149 // If url is a pipe-delimited set of pages, just take the first one. 150 if (url.includes("|")) { 151 url = url.split("|")[0]; 152 } 153 return url; 154 } 155 156 /** 157 * Re-direct the browser to the previous page or a known-safe page if no 158 * previous page is found in history. This function is used when the user 159 * browses to a secure page with certificate issues and is presented with 160 * about:certerror. The "Go Back" button should take the user to the previous 161 * or a default start page so that even when their own homepage is on a server 162 * that has certificate errors, we can get them somewhere safe. 163 */ 164 goBackFromErrorPage(browser) { 165 if (!browser.canGoBack) { 166 // If the unsafe page is the first or the only one in history, go to the 167 // start page. 168 browser.loadURI(this.getDefaultHomePage(browser.ownerGlobal), { 169 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 170 }); 171 } else { 172 browser.goBack(); 173 } 174 } 175 176 /** 177 * This function does a canary request to a reliable, maintained endpoint, in 178 * order to help network code detect a system-wide man-in-the-middle. 179 */ 180 primeMitm(browser) { 181 // If we already have a mitm canary issuer stored, then don't bother with the 182 // extra request. This will be cleared on every update ping. 183 if (Services.prefs.getStringPref("security.pki.mitm_canary_issuer", null)) { 184 return; 185 } 186 187 let url = Services.prefs.getStringPref( 188 "security.certerrors.mitm.priming.endpoint" 189 ); 190 let request = new XMLHttpRequest({ mozAnon: true }); 191 request.open("HEAD", url); 192 request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; 193 request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; 194 195 request.addEventListener("error", event => { 196 // Make sure the user is still on the cert error page. 197 if (!browser.documentURI.spec.startsWith("about:certerror")) { 198 return; 199 } 200 201 let secInfo = request.channel.securityInfo.QueryInterface( 202 Ci.nsITransportSecurityInfo 203 ); 204 if (secInfo.errorCodeString != "SEC_ERROR_UNKNOWN_ISSUER") { 205 return; 206 } 207 208 // When we get to this point there's already something deeply wrong, it's very likely 209 // that there is indeed a system-wide MitM. 210 if (secInfo.serverCert && secInfo.serverCert.issuerName) { 211 // Grab the issuer of the certificate used in the exchange and store it so that our 212 // network-level MitM detection code has a comparison baseline. 213 Services.prefs.setStringPref( 214 "security.pki.mitm_canary_issuer", 215 secInfo.serverCert.issuerName 216 ); 217 218 // MitM issues are sometimes caused by software not registering their root certs in the 219 // Firefox root store. We might opt for using third party roots from the system root store. 220 if ( 221 Services.prefs.getBoolPref( 222 "security.certerrors.mitm.auto_enable_enterprise_roots" 223 ) 224 ) { 225 if ( 226 !Services.prefs.getBoolPref("security.enterprise_roots.enabled") 227 ) { 228 // Loading enterprise roots happens on a background thread, so wait for import to finish. 229 BrowserUtils.promiseObserved("psm:enterprise-certs-imported").then( 230 () => { 231 if (browser.documentURI.spec.startsWith("about:certerror")) { 232 browser.reload(); 233 } 234 } 235 ); 236 237 Services.prefs.setBoolPref( 238 "security.enterprise_roots.enabled", 239 true 240 ); 241 // Record that this pref was automatically set. 242 Services.prefs.setBoolPref( 243 "security.enterprise_roots.auto-enabled", 244 true 245 ); 246 } 247 } else { 248 // Need to reload the page to make sure network code picks up the canary issuer pref. 249 browser.reload(); 250 } 251 } 252 }); 253 254 request.send(null); 255 } 256 257 displayOfflineSupportPage(supportPageSlug) { 258 const AVAILABLE_PAGES = ["connection-not-secure", "time-errors"]; 259 if (!AVAILABLE_PAGES.includes(supportPageSlug)) { 260 console.log( 261 `[Not supported] Offline support is not yet available for ${supportPageSlug} errors.` 262 ); 263 return; 264 } 265 266 let offlinePagePath = `chrome://browser/content/certerror/supportpages/${supportPageSlug}.html`; 267 let triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); 268 this.browser.loadURI(offlinePagePath, { triggeringPrincipal }); 269 } 270 271 receiveMessage(message) { 272 switch (message.name) { 273 case "Browser:EnableOnlineMode": 274 // Reset network state and refresh the page. 275 Services.io.offline = false; 276 this.browser.reload(); 277 break; 278 case "Browser:OpenCaptivePortalPage": 279 this.browser.ownerGlobal.CaptivePortalWatcher.ensureCaptivePortalTab(); 280 break; 281 case "Browser:PrimeMitm": 282 this.primeMitm(this.browser); 283 break; 284 case "Browser:ResetEnterpriseRootsPref": 285 Services.prefs.clearUserPref("security.enterprise_roots.enabled"); 286 Services.prefs.clearUserPref("security.enterprise_roots.auto-enabled"); 287 break; 288 case "Browser:ResetSSLPreferences": 289 let prefSSLImpact = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => { 290 return prefs.concat(Services.prefs.getChildList(root)); 291 }, []); 292 for (let prefName of prefSSLImpact) { 293 Services.prefs.clearUserPref(prefName); 294 } 295 this.browser.reload(); 296 break; 297 case "Browser:SSLErrorGoBack": 298 this.goBackFromErrorPage(this.browser); 299 break; 300 case "Browser:SSLErrorReportTelemetry": 301 let reportStatus = message.data.reportStatus; 302 Services.telemetry 303 .getHistogramById("TLS_ERROR_REPORT_UI") 304 .add(reportStatus); 305 break; 306 case "GetChangedCertPrefs": 307 let hasChangedCertPrefs = this.hasChangedCertPrefs(); 308 this.sendAsyncMessage("HasChangedCertPrefs", { 309 hasChangedCertPrefs, 310 }); 311 break; 312 case "ReportBlockingError": 313 this.ReportBlockingError( 314 this.browsingContext.id, 315 message.data.scheme, 316 message.data.host, 317 message.data.port, 318 message.data.path, 319 message.data.xfoAndCspInfo 320 ); 321 break; 322 case "DisplayOfflineSupportPage": 323 this.displayOfflineSupportPage(message.data.supportPageSlug); 324 break; 325 case "Browser:CertExceptionError": 326 switch (message.data.elementId) { 327 case "viewCertificate": { 328 let window = this.browser.ownerGlobal; 329 330 let securityInfo = this.getSecurityInfo( 331 message.data.securityInfoAsString 332 ); 333 let certChain = securityInfo.failedCertChain; 334 let certs = certChain.map(elem => 335 encodeURIComponent(elem.getBase64DERString()) 336 ); 337 let certsStringURL = certs.map(elem => `cert=${elem}`); 338 certsStringURL = certsStringURL.join("&"); 339 let url = `about:certificate?${certsStringURL}`; 340 window.switchToTabHavingURI(url, true, {}); 341 break; 342 } 343 } 344 } 345 } 346} 347