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