Reputation: 288290
I want to call a function with a custom thisArg
.
That seems trivial, I just have to call call
:
func.call(thisArg, arg1, arg2, arg3);
But wait! func.call
might not be Function.prototype.call
.
So I thought about using
Function.prototype.call.call(func, thisArg, arg1, arg2, arg3);
But wait! Function.prototype.call.call
might not be Function.prototype.call
.
So, assuming Function.prototype.call
is the native one, but considering arbitrary non-internal properties might have been added to it, does ECMAScript provide a safe way in to do the following?
func.[[Call]](thisArg, argumentsList)
Upvotes: 9
Views: 2871
Reputation: 179116
At some point you have to trust what's available on the window. It either means caching the functions you're planning on using, or attempting to sandbox your code.
The "simple" solution to calling call
is to temporarily set a property:
var safeCall = (function (call, id) {
return function (fn, ctx) {
var ret,
args,
i;
args = [];
// The temptation is great to use Array.prototype.slice.call here
// but we can't rely on call being available
for (i = 2; i < arguments.length; i++) {
args.push(arguments[i]);
}
// set the call function on the call function so that it can be...called
call[id] = call;
// call call
ret = call[id](fn, ctx, args);
// unset the call function from the call function
delete call[id];
return ret;
};
}(Function.prototype.call, (''+Math.random()).slice(2)));
This can then be used as:
safeCall(fn, ctx, ...params);
Be aware that the parameters passed to safeCall will be lumped together into an array. You'd need apply
to get that to behave correctly, and I'm just trying to simplify dependencies here.
improved version of safeCall
adding a dependency to apply
:
var safeCall = (function (call, apply, id) {
return function (fn, ctx) {
var ret,
args,
i;
args = [];
for (i = 2; i < arguments.length; i++) {
args.push(arguments[i]);
}
apply[id] = call;
ret = apply[id](fn, ctx, args);
delete apply[id];
return ret;
};
}(Function.prototype.call, Function.prototype.apply, (''+Math.random()).slice(2)));
This can be used as:
safeCall(fn, ctx, ...params);
An alternative solution to safely calling call is to use functions from a different window context.
This can be done simply by creating a new iframe
and grabbing functions from its window. You'll still need to assume some amount of dependency on DOM manipulation functions being available, but that happens as a setup step, so that any future changes won't affect the existing script:
var sandboxCall = (function () {
var sandbox,
call;
// create a sandbox to play in
sandbox = document.createElement('iframe');
sandbox.src = 'about:blank';
document.body.appendChild(sandbox);
// grab the function you need from the sandbox
call = sandbox.contentWindow.Function.prototype.call;
// dump the sandbox
document.body.removeChild(sandbox);
return call;
}());
This can then be used as:
sandboxCall.call(fn, ctx, ...params);
Both safeCall
and sandboxCall
are safe from future changes to Function.prototype.call
, but as you can see they rely on some existing global functions to work at runtime. If a malicious script executes before this code, your code will still be vulnerable.
Upvotes: 2
Reputation: 39808
If you trust Function.prototype.call
, you can do something like this:
func.superSecureCallISwear = Function.prototype.call;
func.superSecureCallISwear(thisArg, arg0, arg1 /*, ... */);
If you trust Function..call
but not Function..call.call
, you can do this:
var evilCall = Function.prototype.call.call;
Function.prototype.call.call = Function.prototype.call;
Function.prototype.call.call(fun, thisArg, arg0, arg1 /*, ... */);
Function.prototype.call.call = evilCall;
And maybe even wrap that in a helper.
If your functions are pure and your objects serializable, you can create an iframe and via message passing (window.postMessage
), pass it the function code and the arguments, let it do the call
for you (since it's a new iframe without any 3rd party code you're pretty safe), and you're golden, something like (not tested at all, probably riddled with errors):
// inside iframe
window.addEventListener('message', (e) => {
let { code: funcCode, thisArg, args } = e.data;
let res = Function(code).apply(thisArg, args);
e.source.postMessage(res, e.origin);
}, false);
Same thing can be done with Web Workers.
If that's the case though, you can take it a step further and send it over to your server. If you're running node you can run arbitrary scripts rather safely via the vm module. Under Java you have projects like Rhino and Nashorn; I'm sure .Net has its own implementations (maybe even run it as JScript!) and there're probably a bazillion broken javascript VMs implemented in php.
If you can do that, why not use a service like Runnable to on-the-fly create javascript sandboxes, maybe even set your own server-side environment for that.
Upvotes: 1
Reputation: 48277
That's the power (and risk) of duck typing: if typeof func.call === 'function'
, then you ought to treat it as if it were a normal, callable function. It's up to the provider of func
to make sure their call
property matches the public signature. I actually use this in a few place, since JS doesn't provide a way to overload the ()
operator and provide a classic functor.
If you really need to avoid using func.call
, I would go with func()
and require func
to take thisArg
as the first argument. Since func()
doesn't delegate to call
(i.e., f(g, h)
doesn't desugar to f.call(t, g, h)
) and you can use variables on the left side of parens, it will give you predictable results.
You could also cache a reference to Function.prototype.call
when your library is loaded, in case it gets replaced later, and use that to invoke functions later. This is a pattern used by lodash/underscore to grab native array methods, but doesn't provide any actual guarantee you'll be getting the original native call method. It can get pretty close and isn't horribly ugly:
const call = Function.prototype.call;
export default function invokeFunctor(fn, thisArg, ...args) {
return call.call(fn, thisArg, ...args);
}
// Later...
function func(a, b) {
console.log(this, a, b);
}
invokeFunctor(func, {}, 1, 2);
This is a fundamental problem in any language with polymorphism. At some point, you have to trust the object or library to behave according to its contract. As with any other case, trust but verify:
if (typeof duck.call === 'function') {
func.call(thisArg, ...args);
}
With type checking, you can do some error handling as well:
try {
func.call(thisArg, ...args);
} catch (e) {
if (e instanceof TypeError) {
// probably not actually a function
} else {
throw e;
}
}
If you can sacrifice thisArg
(or force it to be an actual argument), then you can type-check and invoke with parens:
if (func instanceof Function) {
func(...args);
}
Upvotes: 4