aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.4/packages/api-utils/docs/content/proxy.md
blob: 012fbc7f99645706a1581d123ea88217dd534e0a (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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
<!-- contributed by Alexandre Poirot [apoirot@mozilla.com]  -->

Content scripts need access to the DOM of the pages they are attached to.
However, those pages should be considered to be hostile environments: we
have no control over any other scripts loaded by the web page that may be
executing in the same context. If the content scripts and scripts loaded
by the web page were to access the same DOM objects, there are two possible
security problems:

First, a malicious page might redefine functions and properties of DOM
objects so they don't do what the add-on expects. For example, if a
content script calls `document.getElementById()` to retrieve a DOM
element, then a malicious page could redefine its behavior to return
something unexpected:

<pre><code>
// If the web document contains the following script:
document.getElementById = function (str) {
  // Overload indexOf method of all string instances
  str.__proto__.indexOf = function () {return -1;};
  // Overload toString method of all object instances
  str.__proto__.__proto__.toString = function () {return "evil";};
};
// After the following line, the content script will be compromised:
var node = document.getElementById("element");
// Then your content script is totally out of control.
</code></pre>

Second, changes the content script made to the DOM objects would be visible
to the page, leaking information to it.

The general approach to fixing these problems is to wrap DOM objects in
[`XrayWrappers`](https://developer.mozilla.org/en/XPCNativeWrapper)
(also know as `XPCNativeWrapper`). This guarantees that:

* when the content script accesses DOM properties and functions it gets the
original native version of them, ignoring any modifications made by the web
page
* changes to the DOM made by the content script are not visible to scripts
running in the page.

However, `XrayWrapper` has some limitations and bugs, which break many
popular web frameworks. In particular, you can't:

* define attributes like `onclick`: you have to use `addEventListener` syntax
* overload native methods on DOM objects, like this:
<pre><code>
proxy.addEventListener = function () {};
</code></pre>
* access named elements using properties like `window[framename]` or
`document[formname]`
* use some other features that have bugs in the `XrayWrapper`
implementation, like `mozMatchesSelector`

The `proxy` module uses `XrayWrapper` in combination with the
experimental
[Proxy API](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Proxy)
to address both the security vulnerabilities of content scripts and the
limitations of `XrayWrapper`.

<pre>
  /--------------------\                           /------------------------\
  |    Web document    |                           | Content script sandbox |
  | http://mozilla.org |                           |     data/worker.js     |
  |                    | require('content-proxy'). |                        |
  | window >-----------|-     create(window)      -|-> window               |
  \--------------------/                           \------------------------/
</pre>


## The Big Picture ##

The implementation defines two different kinds of proxy:
  
  1. Content script proxies that wrap DOM objects that are exposed to
     content scripts as described above.
  2. XrayWrapper proxies that wrap objects from content scripts before handing
     them over to XrayWrapper functions. These proxies are internal
     and are not exposed to content scripts or document content.

<pre>
  /--------------------\                           /------------------------\
  |    Web document    |                           | Content script sandbox |
  | http://mozilla.org |                           |     data/worker.js     |
  |                    |                   /-------|-> myObject = {}        |
  |                    |  /----------------v--\    |                        |
  |                    |  | XrayWrapper Proxy |    | - document             |
  |                    |  \---------v---------/    \----^-------------------/
  |                    |            v                   |
  |                    |  /-------------\  /----------\ |
  | - document >-------|->| XrayWrapper |<-| CS proxy |-/
  \--------------------/  \-------------/  \----------/
</pre>

Everything begins with a single call to the `create` function exported by the
content-proxy module:

    // Retrieve the unwrapped reference to the current web page window object
    var win = gBrowser.contentDocument.defaultView.wrappedJSObject;
    // Or in addon sdk style
    var win = require("tab-browser").activeTab.linkedBrowser.contentWindow.wrappedJSObject;
    // Now create a content script proxy for the window object
    var windowProxy = require("api-utils/content/content-proxy").create(win);
    
    // We finally use this window object as sandbox prototype,
    // so that all web page globals are accessible in CS too:
    var contentScriptSandbox = new Cu.Sandbox(win, {
      sandboxPrototype: windowProxy
    });

Then all other proxies are created from this one. Attempts to access DOM
attributes of this proxy are trapped, and the proxy constructs and returns
content script proxies for those attributes:

    // For example, if you simply do this:
    var document = window.document;
    // accessing the `document` attribute will be trapped by the `window` content script
    // proxy, and that proxy will that create another content script proxy for `document`

So the main responsibility of the content script proxy implementation is to
ensure that we always return content script proxies to the content script.

## Internal Implementation ##

Each content script proxy keeps a reference to the `XrayWrapper` that enables
it to be sure of calling native DOM methods.

There are two internal functions to convert between content script proxy
values and `XrayWrapper` values.

1. __`wrap`__ takes an XrayWrapper value and wraps it in a content script
proxy if needed.
  This method is called when:
    * a content script accesses an attribute of a content script proxy.
    * XrayWrapper code calls a callback function defined in the content
script, so that arguments passed into the function by the XrayWrapper are
converted into content script proxies. For example, if a content script
calls `addEventListener`, then the listener function will expect any arguments
to be content script proxies.
<br/><br/>
2. __`unwrap`__ takes an object coming from the content script context and:
    * if the object is a content script proxy, unwraps it back to an
XrayWrapper reference
    * if the object is not a content script proxy, wraps it in an XrayWrapper
proxy.
<br/><br/>
This means we can call a XrayWrapper method either with:

        * a raw XrayWrapper object.
    
                // The following line doesn't work if child is a content script proxy,
                // it has to be a raw XrayWrapper reference
                xrayWrapper.appendChild(child)
      
        * an XrayWrapper proxy when you pass a custom object from the content
script context.

                var myListener = {
                  handleEvent: function(event) {
                    // `event` should be a content script proxy
                  }
                };
                // `myListener` has to be another kind of Proxy: XrayWrapper proxy,
                // that aims to catch the call to `handleEvent` in order to wrap its
                // arguments in a content script proxy.
                xrayWrapper.addEventListener("click", myListener, false);


## Stack Traces ##

The following code:

    function listener(event) {
      
    }
    csProxy.addEventListener("message", listener, false);
    
generates the following internal calls:

    -> CS Proxy:: get("addEventListener")
      -> wrap(xrayWrapper.addEventListener)
        -> NativeFunctionWrapper(xrayWrapper.addEventListener)
          // NativeFunctionWrapper generates:
          function ("message", listener, false) {
            return xraywrapper.addEventListener("message", unwrap(listener), false);
          }
          -> unwrap(listener)
            -> ContentScriptFunctionWrapper(listener)
            // ContentScriptFunctionWrapper generates:
            function (event) {
              return listener(wrap(event));
            }

<br>

    // First, create an object from content script context
    var myListener = {
      handleEvent: function (event) {
        
      }
    };
    // Then, pass this object as an argument to a CS proxy method
    window.addEventListener("message", myListener, false);
    
    // Generates the following internal calls:
    -> CS Proxy:: get("addEventListener")
      -> wrap(xrayWrapper.addEventListener)
        -> NativeFunctionWrapper(xrayWrapper.addEventListener)
           // Generate the following function:
           function ("message", myListener, false) {
              return xraywrapper.addEventListener("message", unwrap(myListener), false);
           }
           -> unwrap(myListener)
             -> ContentScriptObjectWrapper(myListener)
                // Generate an XrayWrapper proxy and give it to xrayWrapper method.
                // Then when native code fires an event, the proxy will catch it:
                -> XrayWrapper Proxy:: get("handleEvent")
                  -> unwrap(myListener.handleEvent)
                    -> ContentScriptFunctionWrapper(myListener.handleEvent)
                       // Generate following function:
                       function (event) {
                         return myListener.handleEvent(wrap(event));
                       }


<api name="create">
@function
  Create a content script proxy. <br/>
  Doesn't create a proxy if we are not able to create a XrayWrapper for
  this object: for example, if the object comes from system principal.

@param object {Object}
    The object to proxify.

@returns {Object}
    A content script proxy that wraps `object`.
</api>