-
Notifications
You must be signed in to change notification settings - Fork 113
NiceNeighbor
(legacy summary: How Cajita coexists with untranslated JavaScript)
See SubsetRelationships for context.
On JavaScript as implemented on browsers today (ES3R), inter-object security is not quite defensible against untrusted, untranslated, unverified JavaScript code. Thus, Cajita's security today relies on the assumption that Cajita objects will only be made directly accessible to
- Cajoled code, i.e., the JavaScript code generated by a cajoler -- a trusted Cajita-to-JavaScript compiler.
- Trusted JavaScript, such as cajita.js -- the Cajita runtime.
- Tamed innocent JavaScript, such as the core ES3 library or the browser's DOM API.
The first two are the subject of TranslationTarget. The third, tamed innocent JavaScript, is the subject of this page. This page explains the taming API provided by the Cajita runtime and how to use it to tame innocent JavaScript. Taming is the design of the interface between cajoled and uncajoled JavaScript. The taming is based on whitelisting -- unless an uncajoled property is explicitly made visible by taming, it is not visible to Cajita code. For those properties that are made visible, taming can attenuate what interactions are possible across the boundary.
Innocent code is code written without awareness of the possibility of evil. Innocent code neither seeks to do evil, nor does it engage in any defensive practices needed to resist evil. Although most existing JavaScript code is innocent, without strong evidence to the contrary, we must classify most code as untrusted, i.e., potentially hostile. If we misclassify hostile code as innocent, Cajita's security is lost. We assume innocent code may be buggy, but that none of these bugs can be exploited by an attacker, via the available attack surface, to cause the innocent code to act as hostile code. Again, if we misclassify exploitable code as innocent, Cajita's security is lost.
Some innocent code is directly provided by the target platform -- such as the core ES3 libraries and the browser's DOM API. This code is not subject to examination or translation. For all other innocent code, we do assume that it has been translated through our InnocentCodeRewriter. Note: As of this writing, all deployments of Caja violate this assumption. Once we fix issue 1019, we must repair this unsafe situation.-
Even with the above problem repaired, and even assuming no malicious or exploitable code is misclassified as innocent, previous experiences suggest that taming is the most hazardous part of securing a legacy platform, and bad taming decisions are the most likely source of fatal security problems. The taming of innocent JavaScript must carefully ensure that the security properties enforced by a cajoler together with the Cajita runtime are not violated by innocent JavaScript. These security properties are:
Those invariants enumerated on the CajitaValues page.
JavaScript today has only one almost working security mechanism --
lexical variable capture by nested functions. Although the ES3
specification provides no operations for violating this encapsulation,
neither does it prohibit the addition of such operations by conforming
platforms. ES3R (ES3 as implemented on today's browsers) universally
provides operations -- arguments.caller
, function.caller
, and
function.arguments
-- which allow uncajoled code to violate the
encapsulation of functions on their call stack. Taming should only
allow cajoled code to call uncajoled code when none of the uncajoled
code reachable from those entry points might employ one of these
operations to violate the encapsulation of a Cajita closure on its
call chain.
In the language of the ES3 spec, each property has associated with it
a set of attributes. (To avoid confusion, we will use the ES5 names
for these attributes, even when speaking about ES3.) For example, if
the [[Writable]]
attribute of a property is false, the property is
read-only and cannot be changed by assignment. However, neither ES3
nor ES3R provide any way for JavaScript code to create properties
with non-default attribute settings. But Cajita requires such
restrictions in order to create tamper-proof objects.
Cajita on the ES3R platform simulates attribute-based restrictions of property manipulation by representing such per-property virtual attribute settings in its own bookkeeping. Cajoling translates Cajita code into cajoled JavaScript code that cannot violate the restrictions represented in this bookkeeping. However, innocent JavaScript knows nothing of the restrictions represented in this bookkeeping, and may therefore innocently violate these restrictions.
For example, say malicious Cajita object Mallet has access to both frozen victim Cajita object Fred and to innocent mutating object Inara. Since Mallet is written in Cajita, it is translated into code which cannot directly mutate Fred. However, if Inara were inappropriately tamed, such that Mallet could pass Fred as an argument to one of Inara's mutating operations, then Mallet could get Inara to innocently do the forbidden mutation on his behalf.
These simulated attributes are also used to freeze all the primordial objects. Untamed code within the same frame must not modify any visible primordial state once Cajita code has been allowed to execute in that frame. This cannot be enforced after the fact by taming; it must be enforced by (necessarily fallible) inspection of innocent code.
___.mark*( obj,...)
|
Marks obj as being a particular kind of Cajita object, such as a constructor, and therefore being usable from Cajita in certain ways. |
---|---|
___.grant*( obj, name,...)
|
Grants some kind of access to the name property of obj, perhaps also marking the current value of obj[ name] at the same time. |
For those taming methods that don't fit either of these patterns, see the documentation on that method.
The following operations are only relevant for taming constructed objects or their prototypes. All mentionable properties on records and arrays are implicitly readable and enumerable anyway. If the record or array is not frozen, then all its mentionable properties are also implicitly writable and deletable.
Even on constructed objects, stringified numbers (all X such that
X === String(Number(
X)
) and "length
" are implicitly
whitelisted as well. On an accessible object, access to such
properties cannot be denied.
Current API | Similar ES5 attribute | Common Semantics | Differences |
---|---|---|---|
___.grantRead( obj, name)
|
none | ES5 has no non-readable properties. | This operation makes actual uncajoled properties directly visible to cajoled code. |
___.grantSet( obj, name)
|
{writable:true, enumerable:true} |
Allow the actual property to be changed by assignment. | none |
___.grantEnum( obj, name)
|
{enumerable:true} |
This property name will be enumerated by a for-in loop on obj. | none |
___.grantDelete( obj, name)
|
{configurable:true} |
The property can be deleted. | In ES5, if a property is configurable, its attributes may change at runtime. In Cajita taming, uncajoled code currently can change attributes unconditionally but should normally do so only on initialization. Cajita code can only delete deletable properties; it cannot otherwise manipulate attributes of constructed objects. |
If obj is a constructed object, then these operations apply directly
to own properties of obj. If obj is a prototypical object, then
these operations determine the behavior of the property as inherited
by constructed objects that inherit from that prototype. Since
prototypical objects are implicitly frozen, grantSet
and
grantDelete
make no sense when obj is a prototypical object and
should not be used.
The second column of the table shows the attribute settings that would
appear in an ES5 call to Object.create
or Object.defineProperty
,
or that would be returned by Object.getOwnPropertyDescriptor
. For
example, when uncajoled JavaScript today expresses the taming
decision ___.grantEnum(foo, 'bar')
, the ES5 equivalent would be
Object.defineProperty(foo, 'bar', {enumerable: true})
.
Mozilla JavaScript introduced experimental getters and setters --
a pair of functions associated with a property name which, together,
simulate the property's value. The getter is called when the
property is read. The value returned by the getter is then used as the
value of the read operation. The setter is called when the property
is assigned to, so that it can perform whatever side effects it
chooses. Getters and setters are needed to allow objects written
in JavaScript to emulate the peculiar behavior of DOM objects, where
assignment to an innerHTML
property can cause vast numbers of side
effects.
Some other JavaScript implementations followed Mozilla, with oddly different semantics for various corner cases. ES5 has codified an understandable and mutually agreeable semantics for getters and setters.
Due to implementation constraints of translating Cajita to efficient ES3R, which includes Internet Explorer 6 and 7 where getters and setters are not available, Cajita implements getters and setters as a consequence of fault handling, so that it can optimize normal property access to occur on a fast path, with little checking and no calls on the typical property access. As a consequence, if normal property access, including inheritance, can succeed, no fault has occurred, and no fault handler will get invoked. Only if a normal read operation fails will a get handler, if available, be called. Only if a normal assignment fails will a set handler be called.
Current API | Similar ES5 attribute | Common Semantics | Differences |
---|---|---|---|
___.useGetHandler( obj, name, getter}
|
{get: getter}
|
Reading the property obtains the result of calling getter.call( obj)
|
Cajita handlers are only tried after normal data lookup fails. |
___.useSetHandler( obj, name, setter}
|
{set: setter}
|
Assigning val to the property calls setter.call( obj, val) . The value of the assignment expression is val, not the result of the setter call. |
Cajita handlers are only tried after normal assignment fails. ES5 represents a non-writable accessor property by {set:undefined} . The Cajita equivalent would be a setter that always throws. |
One useful taming technique is to provide a virtual version of an
actual property by not doing a grantRead
on the actual property, so
normal access fails, and to install getters and setters to simulate
the property as it should be seen by Cajita code. This technique works
only on constructed objects. On records and arrays, since all
mentionable properties are implicitly whitelisted, the actually
property will always be accessed first preventing fault
handling. However, getters and setters can still work of records and
arrays in order to simulate properties that are not actually present.
Of the functions not defined by Cajita code, the only ones that should be accessible to Cajita code as values are those tamed as frozen constructors or as frozen simple-functions. In all cases, if optName is provided, it will be used for debugging purposes as the name of the function.
Current API | Meaning |
---|---|
___.markCtor( fun, optSuper, optName)
|
Marks fun as a constructor -- it can only be called with new . Returns fun. |
___.extend( hiddenFun, someSuper, optName)
|
Use when hiddenFun is a constructor which should not be exposed that makes tamed instances that should be exposed. Returns a new inert tamed constructor which will not make anything, but will be be instanceof -equivalent to hiddenFun. |
___.markFuncFreeze( fun, optName)
|
Marks fun as a simple-function -- one that does not mention this -- and freeze it. A simple function can be called as a function, method, or constructor. It is first class -- reading a readable property whose value is a simple-function obtains the simple-function itself. |
If fun is a constructor and fun.prototype
inherits directly from
aSuper.prototype
, then the optSuper argument to ___.markCtor
should be aSuper. optSuper must be another function marked as a
constructor. optSuper may only be absent or undefined
when
fun.prototype
inherits from nothing. Currently, this is only the
case for Object.prototype
itself.
___.extend
should be called before hiddenFun.prototype
is
initialized, since it will replace hiddenFun.prototype
with a
prototypical object that inherits from
someSuper.prototype
. The someSuper argument of ___.extend
may either be a hidden constructor used as the first argument of
a previous call to ___.extend
, or it may be the tamed inert
constructor returned from a previous such call. If the returned
inert constructor is to be made available to cajoled code under
some name, optName should be that name. For example, Domita's
TameElement
makes tamed wrappers for real HTMLElements, so
Domita exposes
nodeClasses.HTMLElement = ___.extend(TameElement, TameBackedNode, 'HTMLElement');
which denies cajoled access to the TameElement
constructor; but
allows the caja expression node instanceof HTMLElement
to succeed if
node was made by new TameElement(
...)
.
The remaining common use of JavaScript functions is as methods. The
JavaScript expression a.foo(b)
is syntactically a function call
whose left operand is the property read expression a.foo
. If the
value of a.foo
is a simple-function, then this accounts for its
semantics as well. The first taming call below,
___.grantFunc(
obj,
name)
, both makes obj[
name]
readable
(as if by ___.grantRead(
obj,
name)
), and marks its current
value as a frozen simple-function (as if by
___.markFuncFreeze(
obj[
name],
name)
).
The remaining cases in the table determine the taming of exophoric
methods. To decide which one to use, one must understand the
semantics of the exophoric method in question. These calls do not
make obj[
name]
directly readable, and it must not be made directly readable by
other means, else an exophoric function might become accessible to
Cajita code. Instead, they install an appropriate get handler (as if
by ___.useGetHandler
), which returns a pseudo function, a record
containing apply
, call
, and bind
functions that act like methods
on the exophoric function as bound to that exophoric function. (To be
explained better.)
Note that pseudo functions appear to be normal exophoric functions to Valija code, so only the Cajita programmer need be aware that these reads do not return a function.
Current API | Meaning |
---|---|
___.grantFunc( obj, name)
|
Use when obj[ name] is a safe simple-function, i.e., a function that does not mention this that should be invokable from Cajita. |
___.grantGenericMethod( obj, name)
|
Use when obj[ name]( ...) is safe to call directly from Cajita code, and obj[ name] is safe to use generically -- with its this bound to other objects via apply , call , and bind . For example, most methods on Array.prototype are tamed as generic methods. |
___.grantTypedMethod( obj, name)
|
Use when obj[ name]( ...) is safe to call directly from Cajita code, but when obj[ name] is not safe to use generically. This taming ensures that if its this is bound to other objects via apply , call , and bind , the function is only called if the alternate this inherits from obj. For example, most methods on Date.prototype are tamed as typed methods. |
___.grantMutatingMethod( obj, name)
|
Then the method would mutate its this value, which must therefore be guarded by an isFrozen check. |
___.handleGenericMethod( obj, name, callHandler)
|
All remaining cases, where the callHandler supplies whatever alternate behavior is to be made available to Cajita code. |
Note that only grantFunc
makes the property normally readable. All
the others make the property readable by installing a get handler,
which has the inheritance irregularities explained above. Similarly,
only grantFunc
and grantGenericMethod
make the method directly
callable. The others install call handlers, which are similarly tried
only if direct calling fails.
Let's reexamine each of the security properties Cajita relies on.
Mostly, yes.
Cajita functions will translate to ES5-strict functions. The three
closure-breaking operations of ES3R -- arguments.caller
,
function.caller
, and function.arguments
-- are not available
on strict functions, even from nonstrict functions. In designing the
new meta API for ES5 (the reflective property manipulation methods
on Object
, covered next), the EcmaScript committee was careful not
to introduce any new ways to violate closure encapsulation.
The only reason for the mostly qualifier above is that the ES5 spec, like the ES3 spec before it, contains a grand loophole (chapter 16) that allows an implementation to provide virtually any extension and still claim conformance to the spec.
Yes. But with a new restriction on innocent code.
To be explained.
Between frames, yes. Within a frame, we still prevent privilege escalation.
To be explained.
No. This inability forces us to change our strategy.
To be explained.