summaryrefslogtreecommitdiffstats
path: root/toolkit/actors/WebChannelChild.sys.mjs
blob: 563f2566a0df026fecafad1d9e6a5626e1d99ed3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { ContentDOMReference } from "resource://gre/modules/ContentDOMReference.sys.mjs";

// Preference containing the list (space separated) of origins that are
// allowed to send non-string values through a WebChannel, mainly for
// backwards compatability. See bug 1238128 for more information.
const URL_WHITELIST_PREF = "webchannel.allowObject.urlWhitelist";

let _cachedWhitelist = null;

const CACHED_PREFS = {};
XPCOMUtils.defineLazyPreferenceGetter(
  CACHED_PREFS,
  "URL_WHITELIST",
  URL_WHITELIST_PREF,
  "",
  // Null this out so we update it.
  () => (_cachedWhitelist = null)
);

export class WebChannelChild extends JSWindowActorChild {
  handleEvent(event) {
    if (event.type === "WebChannelMessageToChrome") {
      return this._onMessageToChrome(event);
    }
    return undefined;
  }

  receiveMessage(msg) {
    if (msg.name === "WebChannelMessageToContent") {
      return this._onMessageToContent(msg);
    }
    return undefined;
  }

  _getWhitelistedPrincipals() {
    if (!_cachedWhitelist) {
      let urls = CACHED_PREFS.URL_WHITELIST.split(/\s+/);
      _cachedWhitelist = urls.map(origin =>
        Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin)
      );
    }
    return _cachedWhitelist;
  }

  _onMessageToChrome(e) {
    // If target is window then we want the document principal, otherwise fallback to target itself.
    let principal = e.target.nodePrincipal
      ? e.target.nodePrincipal
      : e.target.document.nodePrincipal;

    if (e.detail) {
      if (typeof e.detail != "string") {
        // Check if the principal is one of the ones that's allowed to send
        // non-string values for e.detail.  They're whitelisted by site origin,
        // so we compare on originNoSuffix in order to avoid other origin attributes
        // that are not relevant here, such as containers or private browsing.
        let objectsAllowed = this._getWhitelistedPrincipals().some(
          whitelisted => principal.originNoSuffix == whitelisted.originNoSuffix
        );
        if (!objectsAllowed) {
          console.error(
            "WebChannelMessageToChrome sent with an object from a non-whitelisted principal"
          );
          return;
        }
      }

      let eventTarget =
        e.target instanceof Ci.nsIDOMWindow
          ? null
          : ContentDOMReference.get(e.target);
      this.sendAsyncMessage("WebChannelMessageToChrome", {
        contentData: e.detail,
        eventTarget,
        principal,
      });
    } else {
      console.error("WebChannel message failed. No message detail.");
    }
  }

  _onMessageToContent(msg) {
    if (msg.data && this.contentWindow) {
      // msg.objects.eventTarget will be defined if sending a response to
      // a WebChannelMessageToChrome event. An unsolicited send
      // may not have an eventTarget defined, in this case send to the
      // main content window.
      let { eventTarget, principal } = msg.data;
      if (!eventTarget) {
        eventTarget = this.contentWindow;
      } else {
        eventTarget = ContentDOMReference.resolve(eventTarget);
      }
      if (!eventTarget) {
        console.error("WebChannel message failed. No target.");
        return;
      }

      // Use nodePrincipal if available, otherwise fallback to document principal.
      let targetPrincipal =
        eventTarget instanceof Ci.nsIDOMWindow
          ? eventTarget.document.nodePrincipal
          : eventTarget.nodePrincipal;

      if (principal.subsumes(targetPrincipal)) {
        let targetWindow = this.contentWindow;
        eventTarget.dispatchEvent(
          new targetWindow.CustomEvent("WebChannelMessageToContent", {
            detail: Cu.cloneInto(
              {
                id: msg.data.id,
                message: msg.data.message,
              },
              targetWindow
            ),
          })
        );
      } else {
        console.error("WebChannel message failed. Principal mismatch.");
      }
    } else {
      console.error("WebChannel message failed. No message data.");
    }
  }
}