Let's say that we have some code that is attempting to tame an
underlying API, such as a DOM, by wrapping each API object. The
function to create a wrapper object might look like:
function tameNode(underlyingNode) {
return {
// This is not an entirely realistic implementation of setting
// an event handler, but it will do for the example.
setOnClick: function (onClick) { underlyingNode.onclick = onClick; },
onClick: function () { underlyingNode.onclick(); }
// Rest of tamed API implementation
};
}
The security of this pattern relies on underlyingNode not being
accessible to clients of the tamed node object. At first glance,
there appears to be no way for underlyingNode to leak via this code
(and in a pure object-capability language, there wouldn't be).
But in fact it can leak if a client passes an exophoric function:
function untrusted(tamedNode) {
var leaked;
// 'self' gets bound to whatever object has the function as a
// property (the intended point of exophoric functions).
var exophoric = caja.xo4a(function (self) { leaked = self; });
tamedNode.setOnClick(exophoric);
tamedNode.onClick(); // or get the user to click it
// now leaked === underlyingNode
}
The main "argument for the defence" would be that the author of
makeTamedNode should have known that setting 'underlyingNode.onclick'
to an untrusted object would potentially leak underlyingNode. If the
author recognizes this, they can avoid the attack as follows, for
example:
setOnClick: function (onClick) {
underlyingNode.onclick = function () { onClick(); };
}
To me this is not a very convincing argument:
First, look how strange it is to have to write
"function () { onClick(); };". Provided that "onClick" is only
ever used by calling it with no parameters (which is true in this
example), "function () { onClick(); };" and "onClick" would be
entirely equivalent in most functional programming languages.
So we should add a comment so that some well-meaning maintainer
does not "optimize" this code -- or so that we have a better chance
of detecting an attempt by a not-so-well-meaning person to
introduce a plausibly deniable security flaw.
setOnClick(onClick) {
// This is not equivalent to '... = onClick', which might
// leak underlyingNode if 'onClick' is exophoric.
underlyingNode.onclick = function () { onClick(); };
}
Now a trivial one-line function has turned into 4 or 5 lines.
More importantly, the conceptual overhead of understanding what the
comment is trying to say and whether it is correct, compared to the
original code, is arguably more than a factor of 5.
Note that "function () { onClick(); }" fix above does not work if
the value of the property might or might not be a function. In that
case we have to write:
underlying.property =
(typeof x === 'function') ? function () { x(); } : x;
or, if it might be a function with parameters, something like:
underlying.property =
(typeof x === 'function') ?
function () { x.call(null, arguments); } : x;
This is too complicated and verbose to keep writing in application
code. It could be abstracted into a library function:
caja.sanitize = function (x) {
// Passing null as the first argument to .call will cause
// 'this' to be bound to the global object, which should
// cause a run-time error from the rewritten x if it is an
// attached function.
return (typeof x === 'function') ?
function () { x.call(null, arguments); } : x;
};
but such a function is not, IMHO, the right solution. Omitting
a necessary call to caja.sanitize leads to a security flaw. The
reason why it is needed is not easy to explain (I've probably
done a bad job of it) and requires a thorough grasp of Javascript's
'this' binding semantics that *will* escape most new Caja programmers.
That is liable to result in calls to caja.sanitize being sprinkled
like fairy dust around each property setting operation, just in case.
Besides cluttering the code, that would have a measurable performance
and memory cost. If we need a library function to ensure that something
is not an exophoric function (but that is otherwise the identity),
then there is certainly a case for just getting rid of exophoric
functions instead.
Note that makeTamedNode above is not using exophoric functions
itself. (In fact, it is valid Cajita, which should never need
exophoric functions.) So it is paying a non-trivial complexity,
performance, and analysis cost -- and risking an exploitable security
flaw if the analysis is not correct and complete -- to avoid an attack
that results from a feature of the Caja language that it is not using.
There is a significant difference between a feature that requires
great care to write secure code that uses it, and a feature that,
just by its existence in the language, requires great care when writing
code that does *not* use it. The latter, IMHO, simply does not belong
in a security-focussed language.
What about arguments that exophoric functions are needed too much to
get rid of, notwithstanding potential security weaknesses?
Since no other object-capability languages (and indeed, no other
languages I can think of) have exophoric functions, we cannot say
that they are required for expressiveness.
Are they required in order to tame existing Javascript APIs? No.
In the plausible use cases that I can think of, at the point where
an exophoric function is created, it is known what object 'this' is
going to be bound to. That means that a simple function that closes
over that object could be used instead of an exophoric function --
which leads to more straightforward code with a meaning that is clear
to any programmer familiar with lexically scoped languages.
(If there are use cases for which this is not true, I'd like to see
a concrete example.)
I believe exophoric functions (both implicit and explicit) should be
removed from Caja. If that happened quite soon, in advance of the
security review, there would be some time for consequent
simplifications in the spec and the implementation to feed through.
--
David-Sarah Hopwood
The rest of the post calls this 'makeTameNode'.
[...]
> setOnClick(onClick) {
setOnClick: function (onClick) {
> // This is not equivalent to '... = onClick', which might
> // leak underlyingNode if 'onClick' is exophoric.
> underlyingNode.onclick = function () { onClick(); };
> }
[...]
> underlying.property =
> (typeof x === 'function') ?
> function () { x.call(null, arguments); } : x;
function () { x.apply(null, arguments); } : x;
[...]
> caja.sanitize = function (x) {
> // Passing null as the first argument to .call will cause
> // 'this' to be bound to the global object, which should
> // cause a run-time error from the rewritten x if it is an
> // attached function.
> return (typeof x === 'function') ?
> function () { x.call(null, arguments); } : x;
function () { x.apply(null, arguments); } : x;
> };
--
David-Sarah Hopwood
---------- Forwarded message [as amended by later "minor corrections"]
----------
From: David-Sarah Hopwood <david....@industrial-designers.co.uk>
Date: Tue, May 27, 2008 at 8:46 PM
Subject: [Caja] A Case Against Exophoric Functions
To: google-ca...@googlegroups.com
This message elaborates on the security issue with exophoric functions
that was discussed briefly in the Friday meeting.
Let's say that we have some code that is attempting to tame an
underlying API, such as a DOM, by wrapping each API object. The
function to create a wrapper object might look like:
function makeTameNode(underlyingNode) {
function untrusted(tamedNode) {
var leaked;
tamedNode.setOnClick(exophoric);
setOnClick: function(onClick) {
// This is not equivalent to '... = onClick', which might
// leak underlyingNode if 'onClick' is exophoric.
underlyingNode.onclick = function () { onClick(); };
}
Now a trivial one-line function has turned into 4 or 5 lines.
More importantly, the conceptual overhead of understanding what the
comment is trying to say and whether it is correct, compared to the
original code, is arguably more than a factor of 5.
Note that "function () { onClick(); }" fix above does not work if
the value of the property might or might not be a function. In that
case we have to write:
underlying.property =
(typeof x === 'function') ? function () { x(); } : x;
or, if it might be a function with parameters, something like:
underlying.property =
(typeof x === 'function') ?
function () { x.apply(null, arguments); } : x;
This is too complicated and verbose to keep writing in application
code. It could be abstracted into a library function:
caja.sanitize = function (x) {
// Passing null as the first argument to .call will cause
// 'this' to be bound to the global object, which should
// cause a run-time error from the rewritten x if it is an
// attached function.
return (typeof x === 'function') ?
function () { x.apply(null, arguments); } : x;
Your arguments that merely calling "underlying.onclick()" should not
provide the onclick object access to underlying are compelling. To
ease the porting of old code, warts mode will continue to support
implicit exophora for now. But warts are not an official part of Caja.
It should only be turned on for whitelisted modules that have gone
through a manual review, and we will make no claims about the security
of the result.
In light of your objections, we have tentatively decided that the
warts-off Caja TCB has no notion of exophora! (The Caja runtime will
continue to represent and test an internal XO4A___ property, but
warts-off cajoled code will never call ___.xo4a(), and so this
property can never be set by no-wart code.)
The remaining legitimate need for something like exophoric functions
is compatibility with old APIs where a callback-value was being passed
in, to be called back reflectively, i.e., with call, apply, or bind.
In this case, the first argument to call/bind/apply provides the
self-binding explicitly, in a way that's not surprising for an ocap
reading. It isn't really exophoric! One should not be surprised if
"underlying.onclick.call(underlying)" provides onclick access to
underlying.
The remaining hazard would be if typeof <the callback object> ===
'function', in which case one would be justified in assuming that
call, bind, and apply correspond safely to the semantics of normal
non-reflective function calling.
All this can be achieved today by unprivileged Cajita code, with no
changes to any of the basic Caja or Cajita rules, as demonstrated by
the following code, which runs successfully in the testbed with warts
off:
<script type="text/javascript">
var USELESS = caja.freeze({});
function xo4a(callFn) {
return caja.freeze({
call: callFn,
apply: function(self, args) {
return callFn.apply(USELESS, [self].concat(args));
},
bind: function(self, var_args) {
var initArgs = arguments;
return function(var_args) {
return callFn.apply(USELESS, initArgs.concat(arguments));
};
}
});
}
var x = xo4a(function(self, name1, name2) {
return self[name1+name2];
});
caja.enforce(typeof x === 'object');
var obj = {prop: 'hello'};
caja.enforce(x.call(obj, 'p', 'rop') === 'hello');
caja.enforce(x.apply(obj, ['p', 'rop']) === 'hello');
caja.enforce(x.bind(obj)('p', 'rop') === 'hello');
caja.enforce(x.bind(obj, 'p')('rop') === 'hello');
caja.enforce(x.bind(obj, 'p', 'rop')() === 'hello');
obj.x1 = x;
obj.x2 = x.bind(obj);
caja.enforce(obj.x2('p', 'rop') === 'hello');
var failed = false;
try {
obj.x1('p', 'rop')
} catch (e) { failed = true; }
caja.enforce(failed);
</script>
Given the above code, if a pseudo-exophora, as created by the code in
my previous email, were passed in to setOnClick as the onClick
argument, the call to onclick would fail. If the author of
makeTameNode wishes to accommodate pseudo-exophora safely, they might
write instead:
function makeTameNode(underlyingNode) {
var result = {
// This is not an entirely realistic implementation of setting
// an event handler, but it will do for the example.
setOnClick: function (onClick) { underlyingNode.onclick = onClick; },
onClick: function () { underlyingNode.onclick.call(result); }
// Rest of tamed API implementation
};
return result;
}
If instead the author ports the old code naively, they might write the unsafe
onClick: function () { underlyingNode.onclick.call(underlyingNode); }
but the implied danger will then be more visible to security conscious
readers and reviewers.
> The security of this pattern relies on underlyingNode not being
> accessible to clients of the tamed node object. At first glance,
> there appears to be no way for underlyingNode to leak via this code
> (and in a pure object-capability language, there wouldn't be).
> But in fact it can leak if a client passes an exophoric function:
>
> function untrusted(tamedNode) {
> var leaked;
>
> // 'self' gets bound to whatever object has the function as a
> // property (the intended point of exophoric functions).
> var exophoric = caja.xo4a(function (self) { leaked = self; });
>
> tamedNode.setOnClick(exophoric);
>
> tamedNode.onClick(); // or get the user to click it
> // now leaked === underlyingNode
> }
That would now fail.
> The main "argument for the defence" [...]
> To me this is not a very convincing argument: [...]
We're convinced. Thanks!
> The
> reason why it is needed is not easy to explain (I've probably
> done a bad job of it) and requires a thorough grasp of Javascript's
> 'this' binding semantics that *will* escape most new Caja programmers.
Yes. I believe the remaining hazards above do not have this
counter-intuitive nature. We're reduced the problem back down to the
still-quite-tricky normal ocap hazards.
> I believe exophoric functions (both implicit and explicit) should be
> removed from Caja.
Done. Implicit exophora remain in warts, but that's outside Caja.
> If that happened quite soon, in advance of the
> security review, there would be some time for consequent
> simplifications in the spec and the implementation to feed through.
In order to continue support for implicit exophora in wart-mode, we're
leaving in some extra complexity in caja.js that we believe to be
unreachable by warts-free code. It's a shame to pay this price in our
TCB, but I think that's the right tradeoff for now.
Altogether, this is a major vulnerability reduction. Thanks again!
--
Text by me above is hereby placed in the public domain
Cheers,
--MarkM