Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lavamoat/build-system/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -1620,7 +1620,7 @@
"globals": {
"console.log": true,
"console.warn": true,
"process.exit": true
"process": true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broadened process permissions may expose environment variables

Medium Severity

The policy change for browserslist>baseline-browser-mapping from process.exit to process significantly broadens permissions. The full process object includes process.env (environment variables often containing CI secrets, API keys), process.argv, and other sensitive capabilities. Other packages in this policy file use granular permissions like process.exit, process.cwd, or process.stderr.write. If baseline-browser-mapping only needs to exit the process, the broader process permission grants unnecessary access that could be exploited if the package is compromised.

Fix in Cursor Fix in Web

Copy link
Contributor

@MajorLift MajorLift Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Granting unnecessary process.env access to a build process dependency isn't ideal, and the fix looks simple:

The process entry can be removed by adding process.argv as an override (the following passes yarn lavamoat:auto locally):

diff --git a/lavamoat/build-system/policy-override.json b/lavamoat/build-system/policy-override.json
index 512734d809..1f81ec6b2d 100644
--- a/lavamoat/build-system/policy-override.json
+++ b/lavamoat/build-system/policy-override.json
@@ -593,6 +593,12 @@
       "packages": {
         "@babel/core>@babel/types": true
       }
+    },
+    "browserslist>baseline-browser-mapping": {
+      "globals": {
+        "process.exit": true,
+        "process.argv": true
+      }
     }
   }
 }
diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json
index 36f6a625de..79b637c254 100644
--- a/lavamoat/build-system/policy.json
+++ b/lavamoat/build-system/policy.json
@@ -1620,6 +1620,7 @@
       "globals": {
         "console.log": true,
         "console.warn": true,
+        "process.argv": true,
         "process.exit": true
       }
     },

That said, the packages don't seem to be used in any way that could directly expose vulnerabilities, and they're both well maintained by reputable orgs. @davidmurdoch What do you think -- worth the trouble?

@naugtur It looks like policy generation came up with wider permissions than necessary here (top-level global object instead of nested capabilities). Is this expected behavior?

Copy link
Contributor

@naugtur naugtur Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re "worth the trouble" - sha1-hulud has caught very reputable organizations off-guard. It's not whether we trust the org, it's whether we trust the contents of a package on npm ;)
Narrowing down these policies is a very responsible thing to do.

Policy generation is the best-effort estimation of what seems necessary for the package to work. Depending on how it accesses the globals we sometimes can figure out a subset of fields from the AST, but some AST shapes will cause us to defer to the whole global field. We can't generate a policy that's too narrow because it'd force devs to have to add overrides for many common and harmless globals.

The globals that should be preferred for narrowing down are things like process, document, or chrome - the ones that are bags of mixed capabilities. Once we get past the basics of providing a modern version of lavamoat for node, we are planning to focus on the capabilities that need special treatment (at runtime and in policy review)

Please continue to be vigilant about narrowing down policies for the most powerful capabilities. This is great work! 👏 👏 👏

Also, I'm impressed that AI can now understand implications of policy diffs.
Any ideas how to make it better?

Copy link
Contributor

@MajorLift MajorLift Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this can't be narrowed after all due to a typeof process !== undefined in the dependency. :(

Any ideas how to make it better?

We could add relevant information or instructions to AGENTS.md, but it seems like cursorbot and other models already have a pretty good understanding of what lavamoat does just from having docs/lavamoat-policy-review-process.md in context.

I asked which AST shapes would cause deferral to wider permissions here and got this response:

AST Shapes That Cause LavaMoat to Defer to Wider Permissions

LavaMoat's policy generator (lavamoat-tofu) tries to determine the most specific global access path by walking the AST's member expression chain. When it can't follow the chain, it defers to the parent/wider permission.

How the Analysis Works

For an identifier like process, lavamoat-tofu:

  1. Finds all references to global process
  2. For each reference, walks up the parent chain looking for an unbroken sequence of non-computed MemberExpression nodes
  3. Builds a path like ['process', 'env', 'NODE_ENV'] from that chain
  4. If the chain is empty or broken, returns just ['process']

The key function is isNonComputedMemberLikeExpression:

function isNonComputedMemberLikeExpression(node) {
  return !node.computed && isMemberLikeExpression(node);
}

AST Shapes That Cause Deferral

1. typeof Check on Bare Identifier

typeof process !== 'undefined';

AST:

UnaryExpression
├── operator: "typeof"
└── argument: Identifier("process")  ← parent is UnaryExpression, NOT MemberExpression

Why it defers: The process identifier's parent is UnaryExpression, which fails isNonComputedMemberLikeExpression. Result: memberExpressions = [], so path = ['process'].


2. Bare Identifier in Logical/Binary Expressions

process && process.env;

AST:

LogicalExpression
├── operator: "&&"
├── left: Identifier("process")  ← parent is LogicalExpression, NOT MemberExpression
└── right: MemberExpression(process.env)

Why it defers: The left process has LogicalExpression as parent. The right process.env would produce ['process', 'env'], but the left one produces ['process']. LavaMoat then reduces to the topmost (widest) path, keeping just process.


3. Computed Property Access

process['env'];
// or
process[someVariable];

AST:

MemberExpression
├── object: Identifier("process")
├── property: StringLiteral("env") or Identifier(someVariable)
└── computed: true  ← fails isNonComputedMemberLikeExpression!

Why it defers: computed: true fails the check. The chain breaks, so path = ['process'].


4. Identifier Passed as Argument

someFunction(process);

AST:

CallExpression
├── callee: Identifier("someFunction")
└── arguments: [Identifier("process")]  ← parent is CallExpression, NOT MemberExpression

Why it defers: Parent is CallExpression, not MemberExpression. Path = ['process'].


5. Assignment to Variable (Aliasing)

const p = process;
p.env.NODE_ENV; // later usage

AST for const p = process:

VariableDeclarator
├── id: Identifier("p")
└── init: Identifier("process")  ← parent is VariableDeclarator, NOT MemberExpression

Why it defers: The process reference is assigned to p. LavaMoat sees process with parent VariableDeclarator. Path = ['process'].


6. Destructuring

const { env } = process;

AST:

VariableDeclarator
├── id: ObjectPattern({ env })
└── init: Identifier("process")  ← bare reference to get the object to destructure

Why it defers: Same as aliasing - process is referenced without property access. The destructuring analysis in lavamoat-tofu tries to expand this, but a RestElement or computed property in the pattern causes fallback to ['process'].


The Reduction Rule

After analyzing all references, lavamoat-tofu calls reduceToTopmostApiCalls:

// If array contains 'process' and 'process.env' just keep 'process'

So even if some references produce process.env and others produce process, the final policy contains just process since it's the topmost (widest, most permissive) path that covers all accesses.


Real-World Example: baseline-browser-mapping v2.9.11

The code contains:

"undefined"!=typeof process && process.env && ...

This creates:

  • typeof process → path ['process'] (Pattern 1)
  • process && ... → left side produces ['process'] (Pattern 2)
  • process.env → path ['process', 'env']

After reduction: ['process'] wins because it's the topmost path.


Summary Table

Pattern Parent Node Type Can Follow Chain? Result
process.env MemberExpression ✅ Yes process.env
typeof process UnaryExpression ❌ No process
process && x LogicalExpression ❌ No process
process["env"] MemberExpression (computed) ❌ No process
fn(process) CallExpression ❌ No process
const p = process VariableDeclarator ❌ No process
const {env} = process VariableDeclarator ❌ No process

}
},
"bify-module-groups": {
Expand Down Expand Up @@ -1768,6 +1768,8 @@
"path.resolve": true
},
"globals": {
"console.info": true,
"console.trace": true,
"console.warn": true,
"process.env": true,
"process.versions.node": true
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@
"babelify": "^10.0.0",
"bify-module-groups": "^2.0.0",
"browserify": "^17.0.0",
"browserslist": "^4.23.0",
"browserslist": "^4.28.1",
"bs58": "^6.0.0",
"buffer": "^6.0.3",
"chalk": "^4.1.2",
Expand Down
60 changes: 30 additions & 30 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19245,12 +19245,12 @@ __metadata:
languageName: node
linkType: hard

"baseline-browser-mapping@npm:^2.8.9":
version: 2.8.10
resolution: "baseline-browser-mapping@npm:2.8.10"
"baseline-browser-mapping@npm:^2.9.0":
version: 2.9.11
resolution: "baseline-browser-mapping@npm:2.9.11"
bin:
baseline-browser-mapping: dist/cli.js
checksum: 10/969bdc7a54b94dd25e544268feb54380cb34dd7a61112d27bb12f8a79099d9c6cb5aa862d784d6e096da400f7a0099249ef64e2b4b3b110ee30870802e1453e9
checksum: 10/d71fe6693f999ee0bf1fcd72b31c01390f2bfac40d179175817f24cdb11b19d14a102227a8fa28e0a4733a17c780458c17384c75f2227e45b0e8a0080caf6532
languageName: node
linkType: hard

Expand Down Expand Up @@ -19948,18 +19948,18 @@ __metadata:
languageName: node
linkType: hard

"browserslist@npm:^4.12.0, browserslist@npm:^4.23.0, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.5":
version: 4.26.3
resolution: "browserslist@npm:4.26.3"
"browserslist@npm:^4.12.0, browserslist@npm:^4.23.0, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.5, browserslist@npm:^4.28.1":
version: 4.28.1
resolution: "browserslist@npm:4.28.1"
dependencies:
baseline-browser-mapping: "npm:^2.8.9"
caniuse-lite: "npm:^1.0.30001746"
electron-to-chromium: "npm:^1.5.227"
node-releases: "npm:^2.0.21"
update-browserslist-db: "npm:^1.1.3"
baseline-browser-mapping: "npm:^2.9.0"
caniuse-lite: "npm:^1.0.30001759"
electron-to-chromium: "npm:^1.5.263"
node-releases: "npm:^2.0.27"
update-browserslist-db: "npm:^1.2.0"
bin:
browserslist: cli.js
checksum: 10/49add06fd753a2514d84c75a7de8d9fb3d70be675e53b72981d87f0c0ff40d8a8cd0bd92f77400381704be0bf1c9c5c65aef95d03843d69475ff55188aa12124
checksum: 10/64f2a97de4bce8473c0e5ae0af8d76d1ead07a5b05fc6bc87b848678bb9c3a91ae787b27aa98cdd33fc00779607e6c156000bed58fefb9cf8e4c5a183b994cdb
languageName: node
linkType: hard

Expand Down Expand Up @@ -20402,10 +20402,10 @@ __metadata:
languageName: node
linkType: hard

"caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001599, caniuse-lite@npm:^1.0.30001746":
version: 1.0.30001747
resolution: "caniuse-lite@npm:1.0.30001747"
checksum: 10/b101dc4b13cec3827de6eb44ddda93285d1a93d1396b581c96574e4a635b7fb5d8518086ea57be85d05c7e9af70e27c4e5e0994cbaf9c98771a8a16b000c04fa
"caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001599, caniuse-lite@npm:^1.0.30001759":
version: 1.0.30001762
resolution: "caniuse-lite@npm:1.0.30001762"
checksum: 10/d60a589180198ac759fa1775e521862f03503286cc9354144adcd88e76d275f03b2b6e9cab1c606790eee9d6af5cedbedb048ec652f5ff77a2b229992b7c4845
languageName: node
linkType: hard

Expand Down Expand Up @@ -23389,10 +23389,10 @@ __metadata:
languageName: node
linkType: hard

"electron-to-chromium@npm:^1.5.227":
version: 1.5.229
resolution: "electron-to-chromium@npm:1.5.229"
checksum: 10/871665ffee2fcf48fd5027652a681909f7657895daa0ebb1c0c6aff829457dffa0c16aaca98bd892b77a213d17da896deeb6c919492e308c9eb6d0e46b3c1ea7
"electron-to-chromium@npm:^1.5.263":
version: 1.5.267
resolution: "electron-to-chromium@npm:1.5.267"
checksum: 10/05e55e810cb6a3cda8d29dfdeec7ac0e59727a77a796a157f1a1d65edac16d45eed69ed5c99e354872ab16c48967c2d0a0600653051ae380a3b7a4d6210b1e60
languageName: node
linkType: hard

Expand Down Expand Up @@ -33042,7 +33042,7 @@ __metadata:
bn.js: "npm:^5.2.1"
bowser: "npm:^2.11.0"
browserify: "npm:^17.0.0"
browserslist: "npm:^4.23.0"
browserslist: "npm:^4.28.1"
bs58: "npm:^6.0.0"
buffer: "npm:^6.0.3"
chalk: "npm:^4.1.2"
Expand Down Expand Up @@ -34324,10 +34324,10 @@ __metadata:
languageName: node
linkType: hard

"node-releases@npm:^2.0.21":
version: 2.0.21
resolution: "node-releases@npm:2.0.21"
checksum: 10/5344d634b39d20f47c0d85a1c64567fdb9cf46f7b27ed3d141f752642faab47dae326835c2109636f823758afb16ffbed7b0c0fe6f800ef91cec9f2beb4f2b4a
"node-releases@npm:^2.0.27":
version: 2.0.27
resolution: "node-releases@npm:2.0.27"
checksum: 10/f6c78ddb392ae500719644afcbe68a9ea533242c02312eb6a34e8478506eb7482a3fb709c70235b01c32fe65625b68dfa9665113f816d87f163bc3819b62b106
languageName: node
linkType: hard

Expand Down Expand Up @@ -43340,17 +43340,17 @@ __metadata:
languageName: node
linkType: hard

"update-browserslist-db@npm:^1.1.3":
version: 1.1.3
resolution: "update-browserslist-db@npm:1.1.3"
"update-browserslist-db@npm:^1.2.0":
version: 1.2.3
resolution: "update-browserslist-db@npm:1.2.3"
dependencies:
escalade: "npm:^3.2.0"
picocolors: "npm:^1.1.1"
peerDependencies:
browserslist: ">= 4.21.0"
bin:
update-browserslist-db: cli.js
checksum: 10/87af2776054ffb9194cf95e0201547d041f72ee44ce54b144da110e65ea7ca01379367407ba21de5c9edd52c74d95395366790de67f3eb4cc4afa0fe4424e76f
checksum: 10/059f774300efb4b084a49293143c511f3ae946d40397b5c30914e900cd5691a12b8e61b41dd54ed73d3b56c8204165a0333107dd784ccf8f8c81790bcc423175
languageName: node
linkType: hard

Expand Down
Loading