A tamed version of the DOM Level 2 APIs.

Goals

A mechanism for taming the DOM2 EcmaScript wrappers as implemented on widely used browsers that is consistent with object capability discipline.

Glossary

Descendant Node
A node N is a descendant of M iff N is M or N is reachable from M by recursively traversing members of the childNodes list. N cannot be a descendant of M if it is not M and it is an ancestor of N
Virtual Document
A node which prevents root-wards navigation by cajoled code.
Tame
To present an API similar to an existing one, but that follows Object Capability discipline. Caveat: except where outlined in legacy considerations.
Uncontained Node
A node N is not contained in M iff N is not a descendant of M. If N is not contained in M, then N may be a strict ancestor of M or it may be a descendant of a sibling of a strict ancestor of M, or in a disjoint tree.
Virtual Table
A mapping from property names to means by which each property can be read/written/deleted/called. The table also specifies how an object's properties can be enumerated.

Examples

DOMita attenuates a DOM by intercepting reads, writes, and method calls on DOM nodes, and by partitioning the DOM into multiple virtual documents.

Untamed DOM
DOMita / Virtual Docs
Cajoled JavaScript

Below I show an untamed DOM, and several snippets of caja code and I walk through the interactions between the cajoled code and the untamed DOM.

The untamed DOM after loading two modules

<head> <style> /* Styles created as a result of loading module 1. */ .cajaModule-0123___ p { color: purple } /* Styles created as a result of loading module 2. */ .cajaModule-4567___ p { color: pink } /* * These styles do not interfere with one-another since different * virtual documents' bodies have different unmentionable class markers. */ </style> </head> <body> <h1 id="foo">Container Title</h1> <div class="vdoc-doc___" id="module-a-root"> <div class="vdoc-html___"> <div class="vdoc-body___ cajaModule-0123___"> <p><a id=":foo">Module A Link</a></p> </div> </div> </div> <div class="vdoc-doc___" id="module-b-root"> <div class="vdoc-html___"> <div class="vdoc-body___ cajaModule-4567___"> <p><a id=":foo">Module B Link</a></p> </div> </div> </div> </body>

Code example 1 — modifying an element retrieved by ID

var myLink = document.getElementById('foo');
myLink.href = 'foo.png';
myLink.innerHTML = 'My Image';
  1. document is resolved against the gadget's outers object. In the case of module A, this returns a direct reference to <div id="module-a-root" ...>. This binding was initialized by attachDocumentStub when the container was setting up the various gadgets' virtual documents.
  2. cajita.callPub is invoked with the virtual document root and the string 'getElementById' as arguments.
    1. There is no fasttrack bit or call grant present so no early exit there
    2. The node is not a JSON container so that path does not exit
    3. Possible optimization: On Firefox and other browsers that have Node.prototype on the member lookup chain, and on native caja implementations, a vtable___ member points to a vtable which causes the next step to be skipped.
    4. This ends up being handled by the generic handlers which performs the isDomNode() check as defined in vtable and returns the HtmlDocument vtable based on the presence of vdoc-doc___ in the node's class.
  3. The virtual document vtable's getElementById method prepends the argument 'foo' with a colon to move it out of the namespace of IDs and NAMEs used by container code. The vtable getElementById knows to do this since the taming rules specify the type of that parameter as GLOBAL_NAME. The function that does the prefixing is specified in sanitizers.js
  4. After prefixing the ID, the tamed getElementById calls the untamed version of document.getElementById(':foo'). Because of the global nature of GLOBAL names, this may return either of the two elements with id=":foo" above. HTMLDocument.getElementById is specified in tamed-dom.js as having type HTMLElement so the code in sanitizers.js which deals with that type knows to check that the return value is in the same virtual document as the method was invoked on. If it is not, a fallback method is tried which will return the correct virtual document's <a id=":foo">
  5. Control proceeds to the second line in the caja code which assigns foo.png to myLink.href.
  6. A similar vtable lookup is done for the link element. This returns the HTMLLinkElement vtable which defines a setter for the href element. The href property has type URI and so the sanitizer invokes the container's URL policy to validate and possibly rewrite the URL before doing the set. The URL policy can proxy the URL through a server that does additional security checks.
  7. The assignment succeeds, and returns 'foo.png' instead of the actual sanitized version, so that assignments chain properly.
  8. Similarly, the assignment to myLink.innerHTML involves looking up a setter on a vtable and invoking the runtime HTML sanitizer. If the assigned value included a <script> tag instead of the innocuous "Hello World", it would be stripped out at this stage instead of being actually assigned. The HTML sanitizer applies the same ID renaming policy described above.

Code example 2 — building an orphaned DOM subtree and adding it to a document

var myDiv = document.createElement('DIV');
myDiv.appendChild(document.createTextNode('Hello World');
document.body.appendChild(myDiv);
  1. Lookup of document proceeds as described in code example 1 above.
  2. Then document.createElement is resolved in a similar manner to document.getElementById.
  3. The type for the argument to createElement is TAG_NAME which is checked against the HTML element whitelist. Element names like OBJECT are rejected as are unrecognized names.
  4. The return type for createElement is ORPHAN_NODE which side-steps the usual in-same-document check. CAVEAT: under the virtual document system it is not always true that document.createElement('DIV').ownerDocument === document
  5. The text node is created in a similar manner.
  6. document.body resolves to a getter that looks for the descendant of the virtual document that has vdoc class vdoc-body___.
  7. The appendChild method is resolved against the HTMLBodyElement virtual table which just checks that the argument is a valid DOM node and has no vdoc class. It is assumed that anyone with access to an element can move it to another place in the DOM so it does not bother checking that the argument is orphaned.

Code example 3 — traversing the DOM

var ancestors = [];
for (var p1 = document.getElementsByTagName('p')[0]; p1; p1 = p1.parentNode) {
  ancestors.push(p1);
}

The ancestors list will contain the paragraph element, the virtual document body, the virtual HTML node, and nothing else. Specifically it will not include the real BODY or HTML elements.

Code example 4 — using a DOM node from another gadget's document

var myParagraph = channelToOtherGadget.getNode();
myParagraph.style.color = 'blue';
  1. In the first step, one gadget gets a DOM node from another gadget through a channel that was provided by some container mechanism. By allowing access to that DOM node, the other gadget is granting access to any DOM node in the same virtual document.
  2. Style returns a regular caja object that uses the CSS schema to validate set values.

Code example 5 — secure decomposition

// Create a virtual document
var myDoc = document.createVirtualDocument(myElement);
// A virtual document can be added to another tree if it is not rooted
// somewhere.
// Since a virtual document's parentNode is null, granting access to it
// does not grant authority to remove it from the parentNode.
myElement.appendChild(myDoc);
// Pass it to a piece of code of which we are suspicious to prevent the other
// code from unduly interfering with this module's UI.
exportToOtherModule(myDoc.body);

Code organization

tamed-dom.js

The DOM Level 2 Spec is defined in terms of IDL interfaces like

interface Document : Node {
  readonly attribute DocumentType       doctype;
  readonly attribute DOMImplementation  implementation;
  readonly attribute Element            documentElement;
  Element            createElement(in DOMString tagName)
                                        raises(DOMException);
  …
and semantic relationships between those interfaces. The tamed-dom.js file provides a mechanism for defining a tame JavaScript implementation of an interface backed by an untamed instance of the interface.

Each IDL interface defines a number of properties common to instances of that interfaces. Some properties are readable, some are settable, and some are callable. Each property also has a signature – a nominal type for an attribute; and a function signature consisting of a single nominal return type, zero or more nominal parameter types, and zero or more nominal exception types.

Each interface also fits into an inheritance graph (forest actually). The inheritance hierarchies in this forest are Node, NodeList, Event, DOMException. The DOMImplementation is not tamed by this module.

To provide tame interface constructors, we need to specify several things:

sanitizers.js

Defining an interface via tamedDom.tameInterface adds another type to the set of types usable in the taming declarations.

sanitizers.js provides a limited number of nominal types. When an interface element defined via tamedDom.tameInterface is read or set, the nominal type is used to decide how to vet/sanitize the input, or scrub/tame the output.

vtable.js

Defines a mapping from javascript objects to vtables or null, and generic Cajita property handlers that lookup vtables for properties that cannot otherwise be found. A vtable is a mapping from property names to read/write/call/delete/enum handlers.

vdoc.js

The vdoc.js file defines a mechanism by which a container can separate a DOM into multiple virtual documents, so that a tame node's parentNode, documentElement, and similar properties do not allow navigation past a specially marked DIV element. See Legacy Considerations for details.

This file defines several operations grouped together under the vdoc namespace.

Legacy Considerations

Parent navigation

Object capability discipline requires that a client of the API be able to reason about the amount of authority they are granting away by giving a reference to an object. For it to be effective there has to be a way to grant partial authority. In DOMita, passing a reference to a node, grants the authority to traverse, and perhaps modify, that DOM subtree.

The parentNode and ownerDocument members of the Node interface and some sub-interface members (e.g. HTMLInputElement::form) can allow upward navigation which would allow code that received a node to expand the sub-tree granted perhaps to include the entire document. But a lot of existing code uses parentNode.

The ownerDocument getter returns the closest containing virtual document.

There is a legitimate use of parentNode that cannot be restricted by the virtual document scheme. The DOM tree mutators (e.g. Node::appendChild) use the following language

appendChild
Adds the node newChild to the end of the list of children of this node. If the newChild is already in the tree, it is first removed.

If a failure to reach a parent returned null when a cursor traversal exceeds the virtual document's scope, then existing idioms for checking whether appendChild will modify a distant DOM node

assert(myNode.parentNode == null); myOtherNode.appendChild(myNode);
would falsely suggest that the operation would have less far-reaching side-effects than it does.

But since we have virtual documents at well defined locations, we can maintain the fiction that there is no parent without breaking common idioms.

GUI Focus

Having the authority to add a key listener to a node is necessary but not sufficient to receive key events. That node must also have focus. But the focus() method of HTMLInputElement and friends grabs focus. If we were to allow any piece of code to grab keyboard focus, then it could steal input intended for another widget of code in the same page.

But proper focus handling is critical to usability in web applications, so we need to provide a functional focus() mechanism.

The user implicitly conveys authority to receive input when they interact with a piece of code, e.g. by clicking on it, or using the TAB key to focus on part of it.

So the tamed API presents the same focus() method, but attenuates it by making it fail unless the code that calls it was called inside an event handler. This means that the authority to add a UI event handler (e.g. addEventListener('onclick', myFn)) implies the authority to parlay received events into the authority to grant focus to any node reachable by the event handler.

This approach is similar to the way browsers restrict window.open. To prevent runaway popups, window.open fails unless the user interacted with a page element recently. This approach works well for legacy code, and gives the end user some level of confidence that they are not giving information to some entity that they never interacted with intentionally.

Node Type Constructors

Section 1.1.2 of DOM2 Core says

Most of the APIs defined by this specification are interfaces rather than classes. That means that an implementation need only expose methods with the defined names and specified operation, not implement classes that correspond directly to the interfaces. This allows the DOM APIs to be implemented as a thin veneer on top of legacy applications with their own data structures, or on top of newer applications with different class hierarchies.
but most browsers expose the interface types as JavaScript functions so that they can be used in instanceof (e.g. myNode instanceof HTMLDivElement) checks. This is a useful enough feature that some common JS libraries try to provide an equivalent API for compatibility with IE6 which does not have this feature.

Since DOMita does not wrap nodes, we don't need to worry about a node's constructor being callable avoiding checks in document.createElement.

Multiple Documents

A virtual document is represented in the untamed DOM by markup like the below:
<div class="vdoc-doc___"><!-- Corresponds to the document -->
  <div class="vdoc-html___"><!-- Corresponds to the HTML element -->
    <div class="vdoc-body___"><!-- Corresponds to the BODY element -->
      Virtual document body
    </div>
  </div>
</div>
The vdoc's virtual HTML and BODY elements cannot be removed from their parent, appear to have no attributes, and cannot have attributes added.

The tamed parentNode attribute of a virtual document (a node n for which vdoc.getVdocClass(n) returns 'doc') is null as specified in DOM2. This means that virtual documents serve as limits on root-wards navigation.

DOM2 specifies that for all nodes n, and i in [0, n.childNodes.length), n.childNodes[i] instanceof Node && n.childNodes[i].parentNode === n. CAVEAT: If a virtual document is nested inside another, it is possible that this will not hold. This, and the fact that a non-root node may have nodeType === 9 /* DOCUMENT */ may confuse legacy code that assumes otherwise. Containers may decline to provide a mechanism by which cajoled code can create virtual documents, which should ensure that legacy code assumptions are not violated. Containers that elect to allow this should document it as a quirk.

Since virtual documents limit root-wards navigation, a reference to a node encapsulates authority to any node in the same virtual document or any contained virtual document, but not to any in a containing or disjoint document.

Attenuated DOM Nodes

We cannot wrap DOM nodes without breaking EQ in a way that would either introduce memory leaks, or require Cajita to virtualize the ===, !==, ==, !=, instanceof which would seriously complicate supporting and container scripts.

So we use Cajita property handlers to attenuate the host objects that implement browsers' DOM bindings.

When cajoled code attempts a property read/write/call/enumeration, it does the following:

  1. Fastpath check
  2. Grant check
  3. Handler check

The fastpath check will pass for array indices and the length member (but we should check on IE6 or IE7 since Nodes do not have Object.prototype on its prototype chain).

On IE, setting properties on DOM nodes fails with an exception, so grants cannot be relied upon. We cannot use direct handlers on DOM nodes either, so below we outline a vtable like scheme using generic handlers. If grants exist on DOM nodes they would circumvent the vtable scheme.

vtable.js defines a vtable scheme, and tamed-dom.js defines the vtables. (vtable.js depends on changes to cajita.js fault handling hooks that have not been made as of this writing.) When a non-fasttracked property access on a DOM node occurs, the following happens:

  1. Generic handler looks up vtable, uses that to find a property handler, and delegates to that.
    var vtable = vtable.lookupVTable(node);
    if (vtable) {
      var property = vtable[propertyName];
      if (property && vtable.hasOwnProperty(propertyName)) {
        // Using this form of invocation allows us to use the same read-fault
        // handler functions we'd use to handle faults on the node itself.
        return property.handleRead.call(node, propertyName);
      }
    }
    …
    
  2. vtable.lookupVTable finds a vtable for a javascript object.
    if (isDomNode(obj)) {
      if (obj.nodeType === 1 /* ELEMENT */) {
        // check vdoc.getClassName() and see whether to use the virtual doc,
        // BODY element, and HTML element vtables.
        // check tagName against the schema
      } else {
        // handle non-element types.
        // check attributes against the schema.
      }
    } else if (isNodeList(obj)) {
      // handle id/name aliases properly
    }
    return null;
    
  3. Checking whether an object is a DOM node is not simple across browsers, but checking whether or not the nodeType property is readonly is a good test:
    function isDomNode(candidate) {
      switch (typeof candidate) {
        case 'object':
          if (candidate === null) { return false; }
          break;
        case 'function': break;
        default: return false;
      }
      var nodeType = candidate.nodeType;
      if (nodeType !== +nodeType) { return false; }
      // If an attacker can invoke Object.watch or define{Getter,Setter}
      // or create their own host objects, they can spoof this step.
      // In ES3.1, properties and objects can be frozen.
      // For that, we'd have to detect the language version
      // and use type checks.
      try {
        candidate.nodeType = null;  // should be read-only for Nodes
      } catch (ex) {
        // An ES3.1 or SpiderMonkey setter can throw an exception
        // after mutating the object, but we have no way to recover
        // from that side-effect.
        return true;
      }
      if (candidate.nodeType === nodeType) { return true; }
    
      candidate.nodeType = nodeType;
      return false;
    }
    

NamedNodeMaps and HTMLCollections, and HTMLOptionsCollections

NamedNodeMaps, HTMLCollections, and HTMLOptionsCollections all act like arrays, but alias property names based on the id or name attribute.

An individual node may be accessed by either ordinal index or the node's name or id attributes.

Node lists and HTML collections appear in a number of places:

We don't need to filter results from any of these since, in no case does a node list contain a node that is not a descendant of a node that was involved in the operation that produced the node list.

Some node types act as if they were HTML containers:

Interface HTMLFormElement
The FORM element encompasses behavior similar to a collection and an element.
Interface HTMLSelectElement
The select element allows the selection of an option. The contained options can be directly accessed through the select element as a collection.
CAVEAT: We will not support this behavior since there is too much risk of namespace collision. Developers can use <FORM>.elements and <SELECT>.options instead, as recommended in JS best practices.

HTML Container member masking

See the discussion at Issue 935.

CLASS, ID, and NAME renaming

Some HTML attributes' values form a namespace that might mask elements in a namespace visible to the container or another gadget.

If the container or another gadget relies on document.getElementById to return a node that it created with a particular ID.

Also, on IE6, IE7, and possibly IE8, the IDs and NAMEs intrude on the global scope effectively treating the window object as a combination HTMLElement and HTMLCollection in the same way as FORM and SELECT elements.

<div id="div"></div>
<form id="formid">
  <!--
    - IDs and NAMEs of form elements inside a form or options inside a select
    - do not intrude on
    - the global scope
    -->
  <input id="inpid_in_formid"/>
  <input name="inpname_in_formid"/>
</form>
<form name="formname">
  <input id="inpid_in_formname"/>
  <input name="inpname_in_formname"/>
</form>
<!--
  - But IDs and NAMEs of form elements outside a form or options inside a select
  - do intrude on the global scope
  -->
<input id="inpid"/>
<input name="inpname"/>
<script>(function () {
  // All the below are true on IE6, IE7, and possibly other versions of IE
  var names = ['div', 'formid', 'inpid_in_formid', 'inpname_in_formid',
               'formname', 'inpid_in_formname', 'inpname_in_formname', 'inpid',
               'inpname'];
  var masked = [];
  var notMasked = [];
  for (var i = 0; i < names.length; ++i) {
    (eval(window[names[i]]) ? masked : notMasked).push(names[i]);
  }
  alert('masked=' + masked + ', notMasked=' + notMasked);
  // On IE6,
  // masked=div,formid,formname,inpid,inpname
  // notMasked=inpid_in_formid,inpname_in_formid,inpid_in_formname,
  //     inpname_in_formname
})();</script>

Our HTML schema defines the types (ID, CLASS, LOCAL_NAME, GLOBAL_NAME) and space separated lists of those types. The LOCAL_NAME, GLOBAL_NAME distinction is only important if we want to try and enforce a different rewriting when an element with a local name is inside an element that scopes it properly (e.g. an INPUT inside a FORM), but maintaining that would be hugely complex since there are so many ways to remove an element from the DOM. We rewrite these attributes using the following scheme:
attribute type value as apparent to cajoled code sanitized value
IDfoo:foo
NAMEfoo:foo
CLASSfoofoo

Cajoled code cannot mention IDs, CLASSes, or NAMEs that end in double-underscore.

Tamed styles do not depend on CLASS rewriting. Instead, there is an unmentionable class related to the module instance ID that can be attached to a virtual document root to enable styling of any nodes under that virtual document's root.

document.getElementById does need to be restricted to the virtual document though. When cajoled code calls document.getElementById, the untamed version is applied to the prefixed ID, and if it falls in that document then the result is returned. Otherwise another strategy is applied to find a node with the given id that is properly contained.

Since we are rewriting INPUT names, we need to virtualize FORM submission. We could try and create a mirror FORM that does not have NAMEs and IDs prefixed, but since most containers are already rewriting FORM ACTIONs to point to a proxy, the proxy can rewrite names as appropriate. If a FORM contains a mixture of container created INPUTs (that don't have prefixed names) and INPUTs created by cajoled code (with prefixed names), the proxy will be able to distinguish based on ID/NAME.

Likely Failure Modes

TODO

See Also

TODO