@doodad-js/safeeval
Version:
doodad-js SafeEval (beta)
615 lines (562 loc) • 19.6 kB
JavaScript
// Copyright 2015-2018 Claude Petit, licensed under Apache License version 2.0
"use strict";
exports.add = function add(modules) {
modules = (modules || {});
modules['Doodad.Tools.SafeEval'] = {
version: '4.1.9b',
create: function create(root, /*optional*/_options, _shared) {
//===================================
// Get namespaces
//===================================
const doodad = root.Doodad,
types = doodad.Types,
tools = doodad.Tools,
locale = tools.Locale,
unicode = tools.Unicode,
safeEval = tools.SafeEval;
//===================================
// Internal
//===================================
const __Internal__ = {
deniedTokensAlways: ['constructor', '__proto__'],
deniedTokensGlobal: [
// eval
'Function', 'eval',
// Other eval
'setTimeout', 'setInterval',
// Arguments and 'this'.
'arguments', 'this',
// Variables
'var', 'const', 'let'
],
constants: ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'],
allDigitsRegEx: /^([0-9]+[.]?[0-9]*([e][-+]?[0-9]+)?|0[xX]([0-9a-fA-F])+|0[bB]([01])+|0[oO]([0-7])+)$/,
newLineChars: ['\n', '\r', '\u2028', '\u2029'],
symbolCachedSafeEvalFn: types.getSymbol('__SAFE_EVAL_FN__'),
symbolCachedSafeEvalOptions: types.getSymbol('__SAFE_EVAL_OPTIONS__'),
};
//===================================
// Native functions
//===================================
//tools.complete(_shared.Natives, {
//});
__Internal__.validateExpression = function(expression, locals, globals, options) {
// TODO: Escape sequences
// TODO: String templates
// TODO: Function in function
// TODO: Class
const preventAssignment = types.get(options, 'preventAssignment', true),
allowFunctions = types.get(options, 'allowFunctions', false), // EXPERIMENTAL
allowNew = types.get(options, 'allowNew', false), // EXPERIMENTAL
allowRegExp = types.get(options, 'allowRegExp', false); // EXPERIMENTAL
if (root.DD_ASSERT) {
root.DD_ASSERT(types.isString(expression), "Invalid expression.");
root.DD_ASSERT(types.isNothing(locals) || types.isJsObject(locals), "Invalid locals.");
root.DD_ASSERT(types.isNothing(globals) || types.isArray(globals), "Invalid globals.");
};
// FUTURE: Automatic AST rewrite of "obj[key]" to "get(obj, key)".
/*
let newExpr = null,
dynamicGetFn = types.get(options, 'dynamicGetFn', null);
*/
let prevChr = '',
isString = false,
isEscape = false,
stringChar = null,
isAssignment = false,
isComment = false,
isCommentBlock = false,
isRegExp = false,
isRegExpFlags = false,
isGlobal = true,
isDot = false,
lastTokens = [],
isFunction = false,
isFunctionArgs = false,
functionArgs = null,
level = {name: ''},
prevLevel = level,
isShift = false,
noPrevChr = false,
maybeObject = false;
const levels = [level];
//const maxSafeInteger = types.getSafeIntegerBounds().max;
const pushLevel = function(name) {
level = {name};
levels.push(level);
};
const popLevel = function(name) {
prevLevel = level;
level = levels.pop();
if (level.name !== name) {
throw new types.AccessDenied("Invalid expression.");
};
};
const validateTokens = function validateTokens() {
/* eslint no-cond-assign: "off" */
let tokenName;
while (tokenName = lastTokens.shift()) {
let deniedToken = '';
if (tools.indexOf(__Internal__.deniedTokensAlways, tokenName) >= 0) {
// Invalid
deniedToken = tokenName;
} else if (isGlobal) {
if (tools.indexOf(__Internal__.deniedTokensGlobal, tokenName) >= 0) {
// Invalid
deniedToken = tokenName;
} else if (__Internal__.allDigitsRegEx.test(tokenName)) {
// Valid
} else if (allowNew && (tokenName === 'new')) {
// Valid
} else if (tools.indexOf(__Internal__.constants, tokenName) >= 0) {
// Valid
} else if (types.has(locals, tokenName)) {
// Valid
} else if (tools.indexOf(globals, tokenName) >= 0) {
// Valid
} else if (isFunction && (tools.indexOf(functionArgs, tokenName) >= 0)) {
// Valid
} else {
deniedToken = tokenName;
};
};
if (deniedToken) {
throw new types.AccessDenied("Access to '~0~' is denied.", [deniedToken]);
};
};
if (tokenName) {
isGlobal = false;
};
};
const curLocale = locale.getCurrent();
let chr = unicode.nextChar(expression);
loopChars: while (chr) {
if (isString) {
if (isEscape) {
// Escaped char
isEscape = false;
} else if (chr.chr === '\\') {
// String escape
isEscape = true;
} else if (chr.chr === stringChar) {
// String closure
isString = false;
noPrevChr = true;
prevChr = '';
isGlobal = false;
};
} else if (isRegExpFlags) {
if ((chr.codePoint < 97) || (chr.codePoint > 122)) { // 'a', 'z'
isRegExpFlags = false;
};
} else if (isRegExp) {
// RegExp
if (isEscape) {
// Escaped char
isEscape = false;
} else if (chr.chr === '\\') {
// Char escape
isEscape = true;
} else if (chr.chr === '/') {
// RegExp closure
isRegExp = false;
isRegExpFlags = true;
noPrevChr = true;
prevChr = '';
};
} else if (isComment) {
if (tools.indexOf(__Internal__.newLineChars, chr.chr) >= 0) { // New line
isComment = false;
validateTokens();
};
} else if (isCommentBlock) {
// Comment block
if ((prevChr === '*') && (chr.chr === '/')) {
// End comment block
isCommentBlock = false;
noPrevChr = true;
prevChr = '';
};
} else if (isAssignment && (chr.chr === '>')) {
// Arrow function
if (!allowFunctions) {
throw new types.AccessDenied("Functions are denied.");
};
if (isFunction) {
// For simplicity
throw new types.AccessDenied("Function in function is denied.");
};
isAssignment = false;
isFunction = true;
if (prevLevel.name === '(') {
functionArgs = lastTokens;
lastTokens = [];
} else if (lastTokens.length > 0) {
functionArgs = [lastTokens.pop()];
} else {
functionArgs = [];
};
} else if ((isAssignment && (chr.chr !== '=')) || (isShift && (chr.chr === '='))) {
// Assignment
if (preventAssignment) {
throw new types.AccessDenied("Assignment is not allowed.");
};
validateTokens();
} else if ((prevChr === '/') && (chr.chr === '/')) {
// Begin statement comment
isComment = true;
noPrevChr = true;
prevChr = '';
} else if ((prevChr === '/') && (chr.chr === '*')) {
// Begin comment block
isCommentBlock = true;
noPrevChr = true;
prevChr = '';
} else if (isGlobal && (lastTokens.length <= 0) && (prevChr === '/') && (chr.chr !== '/')) {
// Begin RegExp
if (!allowRegExp) {
// For simplicity
throw new types.AccessDenied("Regular expressions are not allowed.");
};
isRegExp = true;
if (chr.chr === '\\') {
isEscape = true;
};
noPrevChr = true;
prevChr = '';
} else if ((chr.chr === ';') || (tools.indexOf(__Internal__.newLineChars, chr.chr) >= 0)) { // End of statement
validateTokens();
let hasSemi = false;
do {
if (chr.chr === ';') {
hasSemi = true;
};
chr = chr.nextChar();
} while (chr && ((chr.chr === ';') || (tools.indexOf(__Internal__.newLineChars, chr.chr) >= 0)));
if (!isDot || hasSemi) {
isGlobal = true;
};
if (hasSemi) {
prevChr = '';
};
continue loopChars;
} else if (unicode.isSpace(chr.chr, curLocale)) { // Space
do {
chr = chr.nextChar();
} while (chr && unicode.isSpace(chr.chr, curLocale));
continue loopChars;
} else if ((chr.chr === '$') || (chr.chr === '_') || unicode.isAlnum(chr.chr, curLocale)) {
let tokenName = '';
do {
// Token
tokenName += chr.chr;
chr = chr.nextChar();
} while (chr && ((chr.chr === '$') || (chr.chr === '_') || unicode.isAlnum(chr.chr, curLocale)));
if (isGlobal && (tokenName === 'class')) {
// For simplicity
throw new types.AccessDenied("Classes are denied.");
} else if (isGlobal && (tokenName === 'function')) {
if (!allowFunctions) {
throw new types.AccessDenied("Functions are denied.");
};
if (isFunction || isFunctionArgs) {
// For simplicity
throw new types.AccessDenied("Function in function is denied.");
};
isFunctionArgs = true;
} else if (isFunction && isGlobal && (tokenName === 'return')) {
// Ignore
} else {
lastTokens.push(tokenName);
};
prevChr = '';
continue loopChars;
} else if (['][', '+[', '-[', '![', '~[', '|[', '&[', '*[', '/['].indexOf(prevChr + chr.chr) >= 0) {
// JsFuck or whatever non-sense
throw new types.AccessDenied("Invalid property accessor.");
} else if (chr.chr === '\\') {
// For simplicity
throw new types.AccessDenied("Escape sequences not allowed.");
} else if (chr.codePoint > 0x7F) {
// For simplicity
throw new types.AccessDenied("Invalid character.");
} else if ((level.name === '{') && (chr.chr === ':')) {
lastTokens = [];
} else if ((chr.chr === '"') || (chr.chr === "'")) {
// Begin String
validateTokens();
isString = true;
stringChar = chr.chr;
} else if (chr.chr === '`') {
validateTokens();
// For simplicity.
throw new types.AccessDenied("Template strings are denied.");
} else if (chr.chr === ']') {
popLevel('[');
isGlobal = false;
} else if (chr.chr === '[') {
if (!isGlobal || (lastTokens.length > 0)) {
// FUTURE: Automatic AST rewrite of "obj[key]" to "get(obj, key)".
/*
if (dynamicGetFn === null) {
dynamicGetFn = '__' + types.generateUUID().replace(/[-]/g, '');
locals[dynamicGetFn] = safeEval.get;
};
if (newExpr === null) {
newExpr = expression.slice(0, .......);
};
newExpr += dynamicGetFn + "(" ..................;
*/
throw new types.AccessDenied("Invalid property accessor.");
};
pushLevel('[');
validateTokens();
} else if (chr.chr === '{') {
pushLevel('{');
maybeObject = isGlobal;
isGlobal = true;
isDot = false;
} else if (chr.chr === '}') {
popLevel('{');
isGlobal = false;
maybeObject = false;
} else if (chr.chr === '(') {
if (maybeObject) {
if (!allowFunctions) {
// For simplicity
throw new types.AccessDenied("Functions are denied.");
};
maybeObject = false;
isFunctionArgs = true;
} else {
validateTokens();
};
pushLevel('(');
noPrevChr = true;
} else if (chr.chr === ')') {
popLevel('(');
if (isFunctionArgs) {
isFunction = true;
functionArgs = lastTokens;
lastTokens = [];
};
noPrevChr = true;
prevChr = '';
} else if (((chr.chr === '+') || (chr.chr === '-')) && (prevChr === chr.chr)) {
validateTokens();
// Increment
if (preventAssignment) {
throw new types.AccessDenied("Increment operators are not allowed.");
};
noPrevChr = true;
prevChr = '';
} else if (((chr.chr === '<') || (chr.chr === '>')) && (prevChr === chr.chr)) {
// Potential shift assignment
isShift = true;
} else if ((chr.chr === '=') && ((prevChr !== '>') && (prevChr !== '<') && (prevChr !== '=') && (prevChr !== '!'))) {
// Potential assignment
isAssignment = true;
} else {
if (chr.chr !== ',') {
validateTokens();
};
isAssignment = false;
isShift = false;
if (chr.chr === '.') {
isDot = true;
isGlobal = false;
} else {
isDot = false;
isGlobal = true;
};
};
if (noPrevChr) {
noPrevChr = false;
} else {
prevChr = chr.chr;
};
chr = chr.nextChar();
};
validateTokens();
// FUTURE: Automatic AST rewrite of "obj[key]" to "get(obj, key)".
/*
return (newExpr === null ? expression : newExpr);
*/
};
__Internal__.createEvalFn = function createEvalFn(locals, globals) {
root.DD_ASSERT && root.DD_ASSERT(types.isNothing(locals) || types.isObject(locals), "Invalid locals object.");
if (types.isNothing(globals)) {
globals = [];
} else {
root.DD_ASSERT && root.DD_ASSERT(types.isArray(globals), "Invalid global names array.");
};
globals = tools.reduce(globals, function(locals, name) {
if (name in global) {
locals[name] = global[name];
};
return locals;
}, {});
locals = tools.nullObject(globals, locals);
if (types.isEmpty(locals)) {
return tools.eval;
} else {
return tools.createEval(types.keys(locals)).apply(null, types.values(locals));
};
};
safeEval.ADD('get', root.DD_DOC(
{
author: "Claude Petit",
revision: 0,
params: {
obj: {
type: 'any',
optional: false,
description: "An object.",
},
key: {
type: 'string,symbol',
optional: false,
description: "A key",
},
},
returns: 'any',
description: "Used to safely and dynamically get a value from an obect key. Please provide that function as part of the 'locals' argument of 'safeEval.eval' or 'safeEval.evalCached', and use it in your expresions instead of the '[...]' property accessor operator.",
}
, function get(obj, key) {
// FUTURE: Automatic AST rewrite of "obj[key]" to "get(obj, key)".
if (!types.isString(key) && !types.isSymbol(key)) {
key = types.toString(key);
};
if (__Internal__.deniedTokensAlways.indexOf(key) >= 0) {
throw new types.AccessDenied("Access to '~0~' is denied.", [key]);
};
return obj[key];
}));
safeEval.ADD('eval', root.DD_DOC(
{
author: "Claude Petit",
revision: 8,
params: {
expression: {
type: 'string',
optional: false,
description: "An expression",
},
locals: {
type: 'object',
optional: true,
description: "Local variables.",
},
globals: {
type: 'arrayof(string)',
optional: true,
description: "List of allowed global variables.",
},
options: {
type: 'object',
optional: true,
description: "Options.",
/* TODO: Document them somewhere
preventAssignment: {
type: 'boolean',
optional: true,
description: "If 'true', will prevent assignment operators. Otherwise, it will allow them. Default is 'true'.",
},
allowFunctions: {
type: 'boolean',
optional: true,
description: "IMPORTANT: Experimental, please leave it to 'false' (the default), or report bugs... If 'true', will allow function declarations. Otherwise, it will prevent them. Default is 'false'.",
},
allowNew: {
type: 'boolean',
optional: true,
description: "IMPORTANT: Experimental, please leave it to 'false' (the default), or report bugs... If 'true', will allow the 'new' operator. Otherwise, it will prevent it. Default is 'false'.",
},
allowRegExp: {
type: 'boolean',
optional: true,
description: "IMPORTANT: Experimental, please leave it to 'false' (the default), or report bugs... If 'true', will allow syntactic regular expressions. Otherwise, it will prevent them. Default is 'false'.",
},
*/
},
},
returns: 'any',
description: "Evaluates a Javascript expression with some restrictions.",
}
, function _eval(expression, /*optional*/locals, /*optional*/globals, /*optional*/options) {
// NOTE: Caller functions should use "safeEvalCached" for performance issues (only when expressions are controlled and limited)
// Restrict access to locals (locals={...}) and globals (globals=[...]).
// Prevents access to my local variables and arguments.
// Optionally prevents assignments and increments
__Internal__.validateExpression(expression, locals, globals, options);
const evalFn = __Internal__.createEvalFn(locals, globals);
return evalFn(expression);
}));
safeEval.ADD('evalCached', root.DD_DOC(
{
author: "Claude Petit",
revision: 5,
params: {
evalCacheObject: {
type: 'object',
optional: false,
description: "An object to use as cache",
},
expression: {
type: 'string',
optional: false,
description: "An expression",
},
options: {
type: 'object',
optional: true,
description: "Options.",
},
},
returns: 'any',
description: "Evaluates a Javascript expression with some restrictions, with cache.",
}
, function evalCached(evalCacheObject, expression, /*optional*/options) {
// WARNING: If expressions are not controlled and limited, don't use this function because of memory overhead
// WARNING: Will always uses the same options for the same cache object
if (root.DD_ASSERT) {
root.DD_ASSERT(types.isJsObject(evalCacheObject), "Invalid cache object.");
root.DD_ASSERT(types.isString(expression), "Invalid expression.");
};
expression = tools.trim(types.toString(expression));
let evalFn = evalCacheObject[__Internal__.symbolCachedSafeEvalFn];
let locals,
globals;
if (evalFn) {
options = evalCacheObject[__Internal__.symbolCachedSafeEvalOptions];
locals = types.get(options, 'locals');
globals = types.get(options, 'globals');
} else {
locals = types.freezeObject(types.clone(types.get(options, 'locals')));
globals = types.freezeObject(types.clone(types.get(options, 'globals')));
options = types.freezeObject(tools.extend({}, options, {locals, globals}));
evalFn = __Internal__.createEvalFn(locals, globals);
types.setAttribute(evalCacheObject, __Internal__.symbolCachedSafeEvalFn, evalFn, {});
types.setAttribute(evalCacheObject, __Internal__.symbolCachedSafeEvalOptions, options, {});
};
const notDenied = (__Internal__.deniedTokensAlways.indexOf(expression) < 0);
if (notDenied && types.has(evalCacheObject, expression)) {
return evalCacheObject[expression];
};
__Internal__.validateExpression(expression, locals, globals, options);
root.DD_ASSERT && root.DD_ASSERT(notDenied, "Should have been denied by '__Internal__.validateExpression'.");
const result = evalFn(expression);
if (notDenied) {
evalCacheObject[expression] = result;
};
return result;
}));
//===================================
// Init
//===================================
//return function init(/*optional*/options) {
//};
},
};
return modules;
};