Skip to content

LavaMoat Node.js Runtime

@lavamoat/node (npm, GitHub) provides a runtime environment for LavaMoat-protected Node.js applications.

@lavamoat/node differs from the standard Node.js runtime in that it:

  1. Prevents tampering with the execution environment via SES’ hardening facilities. This completely mitigates prototype-pollution attacks, among other concerns.
  2. Isolates each package’s execution via SES’ Compartments. Packages share no references whatsoever unless explicitly allowed by Policy. This is possible because SES itself—not Node.js—is responsible for loading and executing modules.
  3. Enforces a user-provided Policy. The Policy controls which globals, Node.js APIs and other packages any given package can access. The Policy provides granular control over whether a global (or even a property thereof) is exposed, read-only, or writable.
npm install @lavamoat/node

The first step is to generate a Policy for your application.

@lavamoat/node generates a Policy from your application’s entry point by crawling all reachable modules, detecting globals, Node.js builtins, and references to other packages1.

Creating a Policy differs slightly depending on whether you intend to run your own application2 or a third-party tool under LavaMoat.

To create a Policy for your own application:

Execute the following command from your project’s root directory:

npx lavamoat generate <app.js>

…where <app.js> is the entry point of your application.

To create a Policy for a third-party tool:

Execute the following command from your project’s root directory:

npx lavamoat generate --bin <executable>

…where <executable> is the tool’s executable3 (e.g., eslint).

In either case, you should see some output like the following:

info › Generating LavaMoat Policy from ./dist/index.js…
info › Crawling dependency graph…
info › ✓ Found 1437 packages in ~0.61s
info › ✓ Inspected 160 (~26.47 modules/s)
warning › source-map-support is not a builtin module, but was otherwise not accessible from typescript. This may be due to an implicit dependency.
warning › Package cosmiconfig>import-fresh contains potential SES violations at the following location:
warning › ▶ /path/to/node_modules/import-fresh/index.js:33:63 — dynamic requires
warning › Package typescript contains potential SES violations at the following location:
warning › ▶ /path/to/node_modules/typescript/lib/typescript.js:8397:27 — dynamic requires
info › ✓ Policy for ./dist/index.js generated in ~6.82s
info › ✓ Wrote Policy to ./lavamoat/node/policy.json

The Policy file will be written to ./lavamoat/node/policy.json, which is the default location4.

The above example output shows warnings. Warnings indicate potential runtime failures, and generally shouldn’t be ignored. Check out the Troubleshooting Guide for more information. If you see warnings, it’s likely you’ll need to create a Policy Override file to explicitly grant access to the resources your application needs.

Once we have a Policy and/or Policy Override file, we can now move on to actually running the application.

To execute your application under LavaMoat:

npx lavamoat <app.js> -- --flag1 --flag2

…where <app.js> is the entry point of your application and --flag1 --flag2 are any flags your application needs5.

With any luck, you should see your application running as expected. If you’re seeing unexpected failures, then there’s more work to do; read about creating a Policy Override and our troubleshooting guide for more information.

@lavamoat/node’s two main modes of operation are Policy generation and execution:

generate

Generate a Policy for the application.

run

Run the application with the given Policy.

When no command is provided, run is @lavamoat/node’s default command.

Both commands (run and generate) support the following flags.

When using LavaMoat to run multiple applications or tools, you’ll want to keep separate Policy and Policy Override files for each application or tool; this is done by providing the following flags.

--policy/-p

Path to Policy file; in the case of Policy generation, this is the path to write, and in the case of execution, this is the path to read.

The default location for the Policy file is ./lavamoat/node/policy.json.

--policy-override/-o

Path to Policy override file; similar to --policy.

The default location for the Policy Override file is ./lavamoat/node/policy-override.json.

--bin

Specify a third-party executable as the entry point.

This is a boolean flag; if provided, the positional argument is assumed to be an executable in node_modules/.bin/ and @lavamoat/node will find it for you6.

If --bin was provided to generate, --bin must also be provided to run. @lavamoat/node will refuse to execute applications which contain root.usePolicy in a trusted context. The converse is also true; if --bin wasn’t provided to generate, --bin cannot be provided to run.

If either case is detected, @lavamoat/node will throw a TrustMismatchError and fail.

--root/--project-root

Path to the root of the project; used to determine relative paths.

--verbose

Emit verbose output.

--quiet

Emit no output whatsoever.

--help/-h

Show help and exit.

--version/-v

Show version and exit.

These flags are available when using the generate command.

--no-warnings

Disable warnings during Policy generation.

By default, warnings are enabled when generating Policy.

--prod-only

Exclude development dependencies (devDependencies) from the Policy if the entry point accesses them.

By default, development dependencies are included in the Policy if the entry point accesses them.

When using @lavamoat/node to run a third-party tool, you will typically want to avoid this flag.

--no-write

Write Policy to standard output instead of a file.

--warnings-as-errors

Treat warnings as errors. Any warnings will cause the process to exit with code 1.

This can be useful in certain situations, such as running in a CI environment.

If Policy generation is too taxing on your system (hello, CI runners!), you can tradeoff speed for memory by limiting the number of worker threads used.

--concurrency <number>

Limit the number of worker threads to use for Policy generation.

By default, Policy generation uses n - 1 worker threads.

You may see a notice during Policy generation that your Policy override file contains redundant entries; --compact-overrides is the solution. This will help reduce the size of your Policy Override file, and thus its maintenance burden.

--compact-overrides

Remove redundant information from your Policy override file and overwrite it.

Entries which would have been created by analysis alone (i.e., disregarding Policy Overrides entirely) will be removed from your Policy Override file.

These flags are available when using the run command.

--warnings

Enable warnings during execution.

By default, warnings are disabled during execution, as they may interfere with the application’s output.

Scuttling provides an additional layer of security, but is not compatible with all applications.

--scuttle

If provided, the --scuttle flag will scuttle the globalThis object. As another layer of security, this attempts to destroy any global property which is not explicitly allowed by Policy.

Scuttling is applied to the global object (globalThis) after its copy was made, so all of the code running under LavaMoat should never reach the real scuttled properties and instead use the copies.

If code attempts direct access to a global prop outside of LavaMoat’s sandbox, an exception will be thrown.

Scuttling protections come in handy when a Policy allows access to an object which transitively exposes the real globalThis.


  1. Because JavaScript is such a dynamic language, automatic Policy generation using static analysis is considered best-effort. For example, dynamic imports will be unknown until runtime. Additionally, a trusted a package may obfuscate access to APIs and/or globals, and @lavamoat/node will not detect such cases. ↩

  2. If your project is a library, the library itself won’t benefit from LavaMoat, since libraries are not “runnable.” However, you can still benefit by using @lavamoat/node to run third-party tools. ↩

  3. Only project-local JavaScript executables, as installed by a package manager, are supported. You cannot use a globally-installed executable, nor something like /bin/cat! Read more about the --bin flag. ↩

  4. The location of Policy and Policy Override files are configurable via command-line flag ↩

  5. Flags to lavamoat must be provided before the -- separator. ↩

  6. If your package manager does not install executables into node_modules/.bin/, you can provide an absolute or relative filepath to the executable instead. ↩