Reviewing Policy
This guide will show you how to review changes to your LavaMoat Policy File.
Why review Policy?
The Policy File generated by LavaMoat is based on a scan of your codebase, identifying all the powers it uses. The initial policy, resulting from the first time you run policy generation, doesn’t provide security on its own. Instead, it’s your review of the initial policy and the subsequent updates (with new or updated dependencies) that makes your application secure.
Reviewing diffs as dependencies change lets you spot suspicious packages or limit the powers you wish to allow newly added packages to use.
The purpose of the initial review is twofold:
- It helps you build confidence that the current state of your app is not compromised
- You may deny powers to dependencies if you determine they are excessive - not needed for the subset of functionality your app uses.
Reviewing your initial policy may seem like a lot of effort - but think of it as an investment in your application’s security posture.
How to review your policy?
The LavaMoat Policy lists all powers that a package can use; these are the globals and builtin fields.
It also lists which other packages are allowed for the current package to import. You can follow those relations to see whether a package with access to very powerful APIs is used by any suspicious packages as a dependency. See Principle of Least Authority
What to look for when reviewing a Policy diff?
The goal of reviewing the diff is to spot a malicious package being added.
TL;DR
- Check
globalsandbuiltinsfor new powers and investigate if you’re surprised the package would need them - Check if new relationships in
packagesare pointing to packages with very powerful APIs (e.g. spawning child processes in Node.js) - Be aware that the identifier may change to
pkgC>actual-namefrompkgB>pkgA>actual-nameBUT! If the package now also has totally different powers, it’s likely a different package of the same name. Investigate!npm ls actual-nameshould help - When a new package is added, consider limiting its powers to what you actually use
- If you see a big bag of capabilities being exposed (like
documentin the browser orprocessin node.js) always try to narrow it down to a reasonable minimum.
- If you see a big bag of capabilities being exposed (like
Best Practices for Finding Suspicious Changes
First of all - you need to check if any of the packages get access to new powerful APIs unexpectedly.
If a package that was supposed to only be doing basic string operations is suddenly also using fetch and process.env in your build system, you should give it a closer look or add
"fetch": false,"process": falseto the globals field for that package in policy-override.json.
When a new dependency shows up in packages field of packageA: look up what it’s pointing to and if the dependency has access to very powerful APIs; doublecheck whether it makes sense to you that packageA would need to use it.
When dependency tree changes, it’s possible that the dependency nesting might change - so the shortest identifier for one of the resources may now be pkgC>actual-name, not pkgB>pkgA>actual-name.
But there are other more nefarious reasons why that could happen.
If the package now also has totally different powers or dependencies listed it’s likely a different package of the same name. There can be more than one actual-name named package in this case. It could have been introduced as a different version or a totally different package installed from git or as a bundled dependency.
Whn a new package is added, consider limiting its powers to what you actually use.
What to look for in initial review?
The goal of reviewing the initial policy is to spot where packages are given powers that allow them escaping LavaMoat protections or abusing the application.
The minimal viable review is to look at the globals and builtins fields of the policy file to see if any of the packages have access to unexpected powerful APIs.
A more advanced review would be to apply Principle of Least Authority and add entries to policy-override.json to limit the powers of packages to what they actually need to serve your usecase.
A combination of both of the above leads to limiting what is exposed from the most powerful APIs. E.g. process in node.js exposes capabilities to run child processes, import builtin modules synchronously and running more javascript files.
Policy generation is static analysis and may not figure out that all a package needs is process.env. In that case, you need a following override:
"globals": { "process": false, "process.env": true }Powerful APIs
Examples of powerful APIs - not an exhaustive list:
| global | builtin | description |
|---|---|---|
child_process and any form of exec or spawn | Allows running arbitrary commands on the host machine and is not covered | |
fs | Allows reading and writing files on the host machine | |
fetch, XMLHttpRequest, WebSocket, EventSource | http, https, net | Allows making network requests |
document | contains a lot of powerful APIs that can be used to manipulate the DOM, including creating iframes with unprotected globals | |
open | window.open allows opening new windows/tabs and accessing clean globals there | |
navigator | contains a lot of powerful APIs that can be used to fingerprint the user or control the browser | |
chrome or browser | extension APIs - should only be accessed by a package that is a helper library for cross-browser extensions | |
process | Allows reading and writing environment variables and other process-related operations. Exposes functions that allow loading built-in, native and regular JS modules, exposes spawning child processes. | |
vm | Allows running arbitrary code in a new context | |
document.querySelector, document.createElement, etc | Grants access to powerful context related objects such as document and window (aka globalThis) via properties such as ownerDocument or defaultView which are exposed by DOM nodes (which are the type of return values of such APIs) | |
Document.prototype, Node.prototype, etc | Redefining methods of these prototypes may allow attackers to hijack these at runtime when are being used by innocent code elsewhere | |
addEventListener | Events leak powerful objects such as DOM nodes, document and window - such API may grant attackers access to such events. Also, listening to the message event specifically may allow attackers to intercept sensitive messages being sent across the app | |
location | A powerful API that may allow attackers change the location of the app which may result in phishing attempts |
Powerful buffers
Due to how node.js handles buffers, there’s a shared memory pool behind them and every buffer instance is exposing a reference to the entire memory pool. When buffers are passed around in security-critical code, you need to put additional effort into reviewing what has access to any other buffers.
TypedArrays don’t have a shared memory pool, so using them for handling secrets is easier to control.
If you want to avoid many of the risks around using buffers at the cost of the performance of allocating them, you can zero-fill the memory when used:
node --zero-fill-buffersYou can also set Buffer.poolSize to zero early in your program to avoid buffers sharing memory space.
LavaMoat may provide built-in options to tighten memory sharing in puffers in the future.