childNodes
list. N cannot be a descendant of M if it is not M and it is an
ancestor of NDOMita attenuates a DOM by intercepting reads, writes, and method calls on DOM nodes, and by partitioning the DOM into multiple virtual documents.
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.
var myLink = document.getElementById('foo'); myLink.href = 'foo.png'; myLink.innerHTML = 'My Image';
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.
cajita.callPub
is invoked with the virtual document root
and the string 'getElementById'
as arguments.
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.
isDomNode()
check as defined in
vtable and returns the
HtmlDocument
vtable based on the presence of
vdoc-doc___
in the node's class.
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
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">
foo.png
to myLink.href
.
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.
'foo.png'
instead of
the actual sanitized version, so that assignments chain properly.
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.
var myDiv = document.createElement('DIV'); myDiv.appendChild(document.createTextNode('Hello World'); document.body.appendChild(myDiv);
document
proceeds as described in
code example 1 above.
document.createElement
is resolved in a similar manner
to document.getElementById
.
createElement
is TAG_NAME
which is checked against the HTML element
whitelist. Element names like OBJECT
are rejected
as are unrecognized names.
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
document.body
resolves to a getter that looks for the
descendant of the virtual document that has vdoc class
vdoc-body___
.
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.
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.
var myParagraph = channelToOtherGadget.getNode(); myParagraph.style.color = 'blue';
// 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);
tamed-dom.js
The DOM Level 2 Spec is defined in terms of IDL interfaces like
and semantic relationships between those interfaces. Theinterface Document : Node { readonly attribute DocumentType doctype; readonly attribute DOMImplementation implementation; readonly attribute Element documentElement; Element createElement(in DOMString tagName) raises(DOMException); …
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:
appendChild
, et al. E.g.
that the autofill
property be set for all inputs
modified via the tamed API.
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.
vdoc.getVdocClass(node)
— given an element
with a class vdoc-<XYZ>___
returns
<XYZ>
. These are used to mark special nodes in
virtual documents. Returns null
if no such class exists. This method also treats real documents
as virtual documents, so it will return 'body'
if
node is a BODY
element without
a vdoc-<XYZ>___
class, and will
return 'doc'
for a document node (though not for a
document fragment).
vdoc.createVirtualDocument()
— returns a
virtual document root.
vdoc.getVirtualDocumentBody(node)
— given a
node, returns the "body" of the closest containing virtual
document, where "body" is defined as a node n
for
which vdoc.getVdocClass(n) === 'body'
.
Returns null
if no such node exists. Even
though vdoc.getVdocClass
equivocates between real and
virtual documents, the value returned is always an element or null.
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.
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.
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
function
s 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
.
<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.
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:
The fastpath check will pass for array indices and the
length
member (but we should check on IE6 or IE7
since Node
s 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:
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); } } …
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;
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; }
NamedNodeMap
s and
HTMLCollection
s, and
HTMLOptionsCollection
sNamedNodeMaps, 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:
Node.childNodes
Node.attributes
document.anchors
document.applets
document.forms
document.images
document.links
<FORM>.elements
<MAP>.areas
<SELECT>.options
{<TABLE>,<TBODY>,<TFOOT>,<THEAD>}.rows
<TABLE>.tBodies
<TR>.cells
document.getElementsByClassName
document.getElementsByName
document.getElementsByTagName
Some node types act as if they were HTML containers:
Interface HTMLFormElementTheFORM
element encompasses behavior similar to a collection and an element.
Interface HTMLSelectElementCAVEAT: We will not support this behavior since there is too much risk of namespace collision. Developers can useThe select element allows the selection of an option. The contained options can be directly accessed through the select element as a collection.
<FORM>.elements
and
<SELECT>.options
instead, as recommended in
JS best practices.
CLASS
, ID
, and NAME
renamingSome 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 |
---|---|---|
ID | foo | :foo |
NAME | foo | :foo |
CLASS | foo | foo |
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.