Dynamism when you need it
Background
Node.js code is composed of CommonJS modules that are linked together
by the builtin require
function, or import
statements
(used by TypeScript) that typically transpile to
require
(modulo experimental features).
require
itself calls Module._load
(code) to
resolve and load code. "The Node.js Way" explains this flow
well.
Unlike import
, require
is dynamic: a runtime value can specify the
name of a module to load. (The EcmaScript committee is considering a
dynamic import
operator, but we have
not included that in this analysis.)
This dynamism is powerful and flexible and enables varied use cases like the following:
- Lazy loading. Waiting to load a dependency until it is definitely needed.
const infrequentlyUsedAPI = (function () { const dependency = require('dependency'); return function infrequentlyUsedAPI() { // Use dependency }; }());
- Loading plugins based on a configuration object.
function Service(config) { (config.plugins || []).forEach( (pluginName) => { require(pluginName).initPlugin(this); }); }
- Falling back to an alternate service provider if the first choice
isn't available:
const KNOWN_SERVICE_PROVIDERS = ['foo-widget', 'bar-widget']; const serviceProviderName = KNOWN_SERVICE_PROVIDERS.find( (name) => { try { require.resolve(name); return true; } catch (_) { return false; } }); const serviceProvider = require(serviceProviderName);
- Taking advantage of an optional dependency when it is available.
let optionalDependency = null; try { optionalDependency = require('optionalDependency'); } catch (_) { // Oh well. }
- Loading a handler for a runtime value based on a naming convention.
function handle(request) { const handlerName = request.type + '-handler'; // Documented convention let handler; try { handler = require(handlerName); } catch (e) { throw new Error( 'Expected handler ' + handlerName + ' for requests with type ' + request.type); } return handler.handle(request); }
- Introspecting over module metadata.
const version = require('./package.json').version;
During rapid development, file-system monitors can restart a node project when source files change, and the application stitches itself together without the complex compiler and build system integration that statically compiled languages use to do incremental recompilation.
Problem
The node_modules
directory does not keep production code separate
from test code. If test code can be require
d in production, then
an attacker may find it far easier to execute a wide variety of other
attacks. See UIR for more details on this.
Node applications rely on dynamic uses of require
and changes that
break any of these use cases would require coordinating large scale
changes to existing code, tools, and development practices threatening
developer experience.
Requiring developers to pick and choose which source files are production and which are test would either:
- Require them to scrutinize source files not only for their project but also for deep dependencies with which they are unfamiliar leading to poor developer experience.
- Whitelist without scrutiny leading to the original security problem.
- Lead them to not use available modules to solve problems and instead roll their own leading to poor developer experience, and possibly LQC problems.
We need to ensure that only source code written with production constraints in mind loads in production without increasing the burden on developers.
When the behavior of code in production is markedly different from that on a developer's workstation, developers lose confidence that they can avoid bugs in production by testing locally which may lead to poor developer experience and lower quality code.
Success Criteria
We would have prevented abuse of require
if:
- Untrusted inputs could not cause
require
to load a non-production source file, - and/or no non-production source files are reachable by
require
, - and/or loading a non-production source file has no adverse effect.
We would have successfully prevented abuse of eval
, new Function
and related operators if:
- Untrusted inputs cannot reach an
eval
operator, - and/or untrusted inputs that reach them cause no adverse affects,
- and/or security specialists could whitelist uses of
eval
operators that are necessary for the functioning of the larger system and compatible with the system's security goals.
In both cases, converting dynamic operators to static before untrusted inputs reach the system reduces the attack surface. Requiring large-scale changes to existing npm modules or requiring large scale rewrites of code that uses using them constitutes compromises DEX.
Current practices
Some development teams use webpack or similar tools to statically bundle server-side modules, and provide flexible transpilation pipelines. That's a great way to do things, but solving security problems only for teams with development practices mature enough to deploy via webpack risks preaching to the choir.
Webpack, in its minimal configuration, does not attempt to skip test files (code). Teams with an experienced webpack user can use it to great effect, but it is not an out-of-the-box solution.
Webpacking does not prevent calls to require(...)
with unintended
arguments, but greatly reduces the chance that they will load
non-production code. As long as the server process cannot read
JS files other than those in the bundle, then a webpacked server
is safe from UIR. This may not be the case if the production
machine has npm modules globally installed, and the server process
is not running in a chroot jail.
A Possible Solution
We present one possible solution to demonstrate that tackling this problem is feasible.
If we can compute the entire set of require
-able sources when
dealing only with inputs from trusted sources, then we can
ensure that the node runtime only loads those sources even when
exposed to untrusted inputs.
We propose these changes:
- A two phase approach to prevent abuse of
require
.- Tweaks to the node module loader that make it easy to dynamically bundle a release candidate.
- Tweaks to the node module loader in production to restrict code loads based on source content hashes from the bundling phase.
- Two different strategies for preventing abuse of
eval
.- JavaScript idioms that can allow many uses of
eval
to load as modules and to bundle as above. - Using JavaScript engine callbacks to
allow uses of
eval
by approved modules.
- JavaScript idioms that can allow many uses of