2

This is front-end only, and not back-end. I also acknowledge that this is a bad idea. At this point I'm just curious.

I have a table of records. I would like the user to be able to enter a JavaScript conditional statement, which is then applied to the table to filter the records.

For example, to filter out records with a name that's less than 6 characters, I might enter:

record.name.length < 6

Without using an external library, the easiest way I've found to do this is with eval. However, in using eval, I of course introduce the risk of the user breaking the code (not a huge concern since this is front-end only, but still a user experience issue).

I would like to sanitize the user input so that it cannot change any values. So far, I believe I only need to do these two things to make eval "safe":

  • Turn any single equals signs = into double or triple equals signs
  • Remove or escape parentheses ( )

With these two items taken care of, is there anything else I need to do to prevent the user input from changing values?

RobertAKARobin
  • 3,933
  • 3
  • 24
  • 46
  • This is such a bad idea. Define your own condition language, and parse that, don't use `eval`. – Barmar Nov 22 '17 at 21:50
  • To apply one of my most favorite internet quotes here: "The client is in the hands of the enemy." It's aimed at multiplayer game development, but applies here as well. If the user screws with the DOM in some way or another (or any other objects), it's their fault. If you want to stick to using JavaScript with `eval()`, just check whether the evaluation actually worked. Just never blindly trust anything the user is able to execute in some way or another. – Mario Nov 22 '17 at 21:50
  • Always remember that there are other ways for the user to screw with the code, even if you're not using eval(), unless this is some kind of isolated/special browser.. – Mario Nov 22 '17 at 21:52
  • Note that [functions can be called without parentheses](https://stackoverflow.com/questions/35949554/invoking-a-function-without-parentheses/35949617#35949617) – trincot Nov 22 '17 at 21:52
  • 1
    @Barmar I'm aware this is a bad idea, but I'm curious about the answer regardless. – RobertAKARobin Nov 22 '17 at 21:53
  • @trincot Not to forget that there *are* functions you might want to call, such as rounding or truncating something. – Mario Nov 22 '17 at 21:53
  • Yeah, what if he wants to do `record.name.substr(0, 2) == 'ab'` – Barmar Nov 22 '17 at 21:54
  • Or even: `a*(b+c) == 12` – trincot Nov 22 '17 at 21:56
  • Fair point. @trincot 's link provided the information I wanted. – RobertAKARobin Nov 22 '17 at 22:11

2 Answers2

10

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)
David Knipe
  • 3,417
  • 1
  • 19
  • 19
0

I am sharing my implementation (based on @David's answer).

Some of the keys of the Window object might break the Function.apply. This is why I've filtered the ones that break. Explanations in the code below as a comment.

// Why is windowKeys not inside function scope? No need. It won't
// be changing on each call. Creating array with +270 items for each eval
// might effect performance.
const windowKeys = Object.keys(window).filter((key) => {
// Why is window filtered?
// There are some cases that parameters given here might break the Function.apply.
// Eg. window keys as numbers: '0', (if there is iframe in the page)
// the ones that starts with numbers '0asdf',
// the ones that has dash and special characters etc.
try {
    Function.apply(null, [key, "return;"]);
    return true;
} catch (e) {
    return false;
}
});

/**
 * evaluates
 * @param {string} code
 * @param {object} context
 * @returns
 */
const safeEval = (code, context) => {
const keys = Object.keys(context);
const allParams = keys.concat(windowKeys, [`"use strict"; return ${code}`]);

try {
    const fn = Function.apply(null, allParams);
    const params = keys.map((key) => context[key]);
    return fn(...params);
} catch (e) {
    console.log(e);
}
};

// simple expression evaluation
const res = safeEval("a + b", { a: 1, b: 2 });
console.log(res);

// try to access window
const res1 = safeEval("{a, b, window, document, this: this}", { a: 1, b: 2 });
console.log(res1);

Idk. if this approach can be exploited, if it does. I think another approach can be running eval on cross-domain iframe and get the result with window messages.

Kerem atam
  • 2,387
  • 22
  • 34