One way of doing this which is safer than eval
is using the Function
constructor. As far as I know, this answer is totally safe, but it's quite possible there's some caveat I don't know or have forgotten, so everyone feel free to reply if I'm wrong.
The Function
constructor allows you to construct a function from its string and a list of argument names. For example, the function
function(x, y) {
return x + y;
}
could be written as
new Function('x', 'y', 'return x + y;')
or simply
Function('x', 'y', 'return x + y;')
Note that although the function body has access to variables declared in the function definition, it cannot access variables from the local scope where the Function
constructor was called; in this respect it is safer than eval
.
The exception is global variables; these are accessible to the function body. Perhaps you want some of them to be accessible; for many of them, you probably don't. However, there is a way round this: declare the names of globals as arguments to the function, then call the function overriding them with fake values. For example, note that this expression returns the global Object
:
(function() { return Object; })()
but this one returns 'not Object'
:
(function(Object) { return Object; })('not Object')
So, to create a function which does not have access to any of the globals, all you have to do is call the Function
constructor on the javascript string, with arguments named after all the globals, then call the function with some innocuous value for all the globals.
Of course, there are variables (such as record
) which you do want the javascript code to be able to access. The argument-name arguments to Function
can be used for this too. I'll assume you have an object called myArguments
which contains them, for example:
var myArguments = {
record: record
};
(Incidentally, don't call it arguments
because that's a reserved word.) Now we need the list of names of arguments to the function. There are two kinds: arguments from myArguments
, and globals we want to overwrite. Conveniently, in client-side javascript, all global variables are properties in a single object, window
. I believe it's sufficient to use its own properties, without prototype properties.
var myArgumentNames = Object.keys(myArguments);
var globalNames = Object.keys(window);
var allArgumentNames = myArgumentNames.concat(globalNames);
Next we want the values of the arguments:
var myArgumentValues = myArgumentNames.map(function(key) {
return myArguments[key];
};
We don't need to do the values part for the globals; if we don't they'll just all be set to undefined
. (Oh, and don't do Object.keys(myArguments).map(...)
, because there's a (small) chance that the array will come out in the wrong order, because Object.keys
doesn't make any guarantees about the order of its return value. You have to use the same array, myArgumentNames
.) Then call the Function
constructor. Because of the large number of arguments to Function
it's not practical to list them all explicitly, but we can get round this using the apply method on functions:
var myFn = Function.apply(null, allArgumentNames.concat([jsString]))
and now we just call this function with the argument list we've generated, again using the apply
method. For this part, bear in mind that the jsString
may contain references to this
; we want to make sure this
doesn't help the user to do something malicious. The value of this
inside the script is the first argument to apply
. Actually that's not quite true - if jsString
doesn't use strict mode, then trying to set this
to undefined
or null
will fail, and this
will be the global object. You can get round this by forcing the script into strict mode (using '"use strict";\n' + jsString
), or alternatively just set this
to an empty object. Like this:
myFn.apply({}, myArgumentValues)