Consider a front-end JavaScript application where menu items needed to be shown or hidden based on somewhat simple logic (roles that user has and some other logical state).
A simple language was introduced to define this logic in a concise human-readable way and every menu item was assigned a string condition, which looks like this: "isLoggedIn() AND NOT role(PARTNER)"
.
In order to actually check this condition, a simple compiler was implemented, which translates this language into a valid JavaScript code string and executes it using eval()
returning the boolean result in the end.
The compiler works by replacing some constructs, like AND
, OR
and NOT
with valid JavaScript equivalents like &&
, ||
and !
using simple regular expressions:
private compileExpression(expression: string) { // Adding commas around function arguments (to make them strings) expression = expression.replace(/(\w+)\((.*?)\)/g, `$1('$2')`); // Replacing "AND" expression = expression.replace(/\sAND\s/g, ' && '); // Replacing "OR" expression = expression.replace(/\sOR\s/g, ' || '); // Replacing "NOT" expression = expression.replace(/(\s|^)NOT\s/g, ' !'); // Prefixing predicates expression = expression.replace(/(\w+)\((.*?)\)/g, 'Π.$1($2)'); return expression; }
This turns "isLoggedIn() AND NOT role(PARTNER)"
into "Π.isLoggedIn() && !Π.role('PARTNER)"
, which is then executed by eval()
:
public matchCondition = (condition: string): boolean => { // Using greek letter "P" for predicate (for shortness and uniqueness) // noinspection NonAsciiCharacters const Π = this.predicates; const expression = this.compileExpression(condition); const result = eval(expression); return result; }
The Π
is an object with simple predicate functions, which return boolean values like isLoggedIn(): boolean
and role(roleName: string): boolean
.
The expressions, that are being translated and executed are stored statically in local object as strings and are not accessible from global context. Also, all expressions are written by the developers and are not coming from users of the application in any way.
Is it safe to use eval()
this way or should it be avoided at all costs (e.g. "eval is evil", "never use eval", etc)?
What are the possible attack vectors, that could be used to compromise such eval usage?
If expression strings will be loaded from the HTTPS server using XHR/Fetch, will it change the situation security-wise?
The reason to introduce such language and not defining the rules in code directly is that it required that these conditions could be defined in string values, e.g. in a JSON file. The other reason is that such language is easier to read at a glance.
eval
are typically hard-to-follow, confusing, and impossible to debug when things break. There are nearly always solutions that are, in all ways, better.