Dynamically bounding eval
If we could provide an API that was available statically, but not dynamically
we could double-check uses of eval
operators.
// API for allowing some eval
var prettyPlease = require('prettyPlease');
// Carefully reviewed JavaScript generating code
var codeGenerator = require('codeGenerator');
let compile;
prettyPlease.mayI(
module,
(evalPermission) => {
compile = function (source) {
const js = codeGenerator.generateCode(source);
return prettyPlease.letMeEval(
evalPermission,
js,
() => ((0, eval)(js)));
};
});
exports.compile = compile;
The prettyPlease
module cannot be pure JavaScript since only the
C++ linker can take advantage of CodeGeneration callbacks
(code) the way CSP does
(code) on the client, but the definition would be
roughly:
// prettyPlease module
(() => {
const _PERMISSIVE_MODE = 0; // Default
const _STRICT_MODE = 1;
const _REPORT_ONLY_MODE = 2;
const _MODE = /* From command line arguments */;
const _WHITELIST = new Set(/* From command line arguments */);
const _VALID_PERMISSIONS = new WeakSet();
const _EVALABLE_SOURCES = new Map();
if (_MODE !== _PERMISSIVE_MODE) {
// Pseudocode: the code-generation callback installed when the
// JavaScript engine is initialized.
function codeGenerationCheckCallback(context, source) {
// source must be a v8::Local<v8::string> or ChakraCore equivalent
// so no risk of polymorphing
if (_EVALABLE_SOURCES.has(source)) {
return true;
}
console.warn(...);
return _MODE == _REPORT_ONLY_MODE;
}
}
// requestor -- the `module` value in the scope of the code requesting
// permissions.
// callback -- called with the generated permission whether granted or
// not. This puts the permission in a parameter name making it
// much less likely that an attacker who controls a key to obj[key]
// can steal it.
module.mayI = function (requestor, callback) {
const id = String(requestor.id);
const filename = String(requestor.filename);
const permission = Object.create(null); // Token used for identity
// TODO: Needs privileged access to real module cache so a module
// can't masquerade as another by mutating the module cache.
if (_MODE !== _PERMISSIVE_MODE
&& requestor === require.cache[filename]
&& _WHITELIST.has(id)) {
_VALID_PERMISSIONS.add(permission);
// Typical usage is to request permission once during module load.
// Removing from whitelist prevents later bogus requests after
// the module is exposed to untrusted inputs.
_WHITELIST.delete(id);
}
return callback(permission);
};
// permission -- a value received via mayI
// sourceToEval -- code to eval. The code generation callback will
// expect this exact string as its source.
// codeThatEvals -- a callback that will be called in a scope that
// allows eval of sourceToEval.
module.letMeEval = function (permission, sourceToEval, codeThatEvals) {
sourceToEval = String(sourceToEval);
if (_MODE === _PERMISSIVE_MODE) {
return codeThatEvals();
}
if (!_VALID_PERMISSIONS.has(permission)) {
console.warn(...);
if (_MODE !== _REPORT_ONLY_MODE) {
return codeThatEvals();
}
}
const countBefore = _EVALABLE_SOURCES.get(sourceToEval) || 0;
_EVALABLE_SOURCES.set(sourceToEval, countBefore + 1);
try {
return codeThatEvals();
} finally {
if (countBefore) {
_EVALABLE_SOURCES.set(sourceToEval, countBefore);
} else {
_EVALABLE_SOURCES.delete(sourceToEval);
}
}
};
})();
and the eval
operators would check that their argument is in the global
set.
Implicit access to eval
is possible because reflective operators can
reach eval
. As long as we can prevent reflective access to
evalPermissions
we can constrain what can be eval
ed. If
evalPermission
is a function parameter, then only arguments
aliases it, so functions that do not mention the special name
arguments
may safely receive one. Most functions do not mention
arguments
. Before whitelisting a module, a reviewer would be wise
to check for any use of arguments
, and for any escape of permissions
or module
.
evalPermission
is an opaque token — only its reference identity
is significant, so we can check membership in a WeakSet
without
risk of forgery.
This requires API changes to existing modules that dynamically use
eval
, but the changes should be additive and straightforward.
It also allows project teams and security specialists to decide on
a case-by-case basis, which modules really need dynamic eval
.
As with synthetic modules, frozen realms may provide a way to further restrict what dynamically loaded code can do. If you're trying to decide whether to trust a module that dynamically loads code, you have more ways to justifiably conclude that it's safe if the module loads into a sandbox restricts to a limited frozen API.