Repository navigation
fix(analyzer): recognize Node's -r alias for --require in NODE_OPTIONS - #655
Conversation
BH3's env_code_execution check for NODE_OPTIONS loader-injection only matched the long-form flags (--require, --import, --loader, --experimental-loader). Node's own --require option has a documented short alias, -r, and NODE_OPTIONS honors it identically (confirmed via process.allowedNodeEnvironmentFlags on Node 22, and by running node directly with NODE_OPTIONS="-r ./preload.js"). A bundled settings.json setting NODE_OPTIONS to "-r ./malicious.js" therefore scanned as SAFE with the same runtime effect as the already-flagged --require form. Signed-off-by: Udaya Tejas <udayatejas2004@gmail.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @udsy19, thank you for closing the -r gap in the BH3 NODE_OPTIONS check and for the clear reproduction in #654!
Value and readiness: Before this change, a bundled settings file with NODE_OPTIONS=-r ./x.js scanned as SAFE, even though Node preloads the module exactly as it does with --require. Adding -r to _NODE_OPTIONS_CODE_FLAGS closes that gap with a one-token change. The two new parametrized cases assert the existing BH3 / HIGH / env_code_execution contract, one with -r alone and one after a benign flag. The existing benign cases (--max-old-space-size=4096, --dns-result-order=ipv4first, empty) are unaffected. It is ready for final maintainer review.
Material findings
- [Non-blocking, follow-up]
src/skillspector/nodes/analyzers/bundled_execution_surface.py:1377-1382(code this PR does not change): Node removes double quotes when it parses NODE_OPTIONS. As a result,"-r" ./x.js,"--require" ./x.jsand--req""uire ./x.jsall preload the module, but none of those tokens equals a flag in the set, so none is reported. This bypass predates the PR and also affects the long flags, so it does not block this fix. It is worth a follow-up issue: split NODE_OPTIONS the way Node does (strip"and handle\escapes) before comparing tokens.
PIC tradeoffs: None identified.
Verification and gaps:
- I traced
_env_value_is_flagged→_env_node_options_runs_codeat headf72d8f8.-rnow matches as a standalone token. -r=…also matches, through the=branch. Node rejects that spelling in NODE_OPTIONS ("is not allowed"), so this is harmless over-matching.--import,--loaderand--experimental-loaderhave no short aliases, as the PR states._NODE_OPTIONS_CODE_FLAGSis the only NODE_OPTIONS check insrc/. It was introduced by #604 (89e9087), and this file has not changed onmainsince the PR's base. The PR merges cleanly with currentmain.- I checked Node's handling of
-r ./preload.js,-r=./preload.jsand the quoted forms with a local Node v26.4.0 and a scratch preload script that I wrote. No contributor code was run. - CI: all 6 checks are green. Per policy, I did not run the tests locally.
Decision: Approved (reviewed head f72d8f8cc8ff955f9d25c67613dc02c247f55e62)
Fixes #654.
Impact: silent-wrong-result
Who reaches this: any user of the CLI or MCP
scan_skillentry point scanning a skill that bundlesa
.claude/settings.jsonor.claude/settings.local.json, via thebundled_execution_surfaceanalyzer's BH3 rule (
src/skillspector/nodes/analyzers/bundled_execution_surface.py).Triggered when: the bundled settings file sets
env.NODE_OPTIONSto a value containing-r,Node's documented short alias for
--require(e.g."-r ./malicious.js")._env_node_options_runs_code()checks the value's whitespace-separated tokens against
_NODE_OPTIONS_CODE_FLAGS, which only listedthe long-form flags (
--require,--import,--loader,--experimental-loader).-ris absent,so the token check never matches.
What the user observes:
scan_skillreports the skill asSAFEwith noBH3finding, even thoughNODE_OPTIONS=-r ./malicious.jsmakes Node load and execute the attacker's module on everysubsequent Node invocation in that environment — the identical runtime effect as
NODE_OPTIONS=--require ./malicious.js, which the same rule already correctly flagsHIGH.Confirmed against a live Node
v22.23.1install (process.allowedNodeEnvironmentFlags.has('-r')istrue, andNODE_OPTIONS="-r ./preload.js" node -e "..."actually runs the preload module); seethe linked issue for the full reproduction.
Fix
Add
-rto_NODE_OPTIONS_CODE_FLAGS. This is the only NODE_OPTIONS-recognized short alias amongthe flags this set models — Node has no short form for
--import/--loader/--experimental-loader— so no other flag needs adding.
Tests
Added two cases to
tests/nodes/analyzers/test_bundled_execution_surface.py::test_bh3_flags_risky_settings_surface:NODE_OPTIONS=-r ./.claude/preload.jsalone, and combined with a benign flag(
--max-old-space-size=512 -r ./.claude/hook.js), both asserting the existingBH3/HIGH/env_code_executioncontract this suite already uses for the long-form flags.Negative control: reverted
bundled_execution_surface.pyto its pristine state and re-ran the twonew cases:
With the fix restored, the full
tests/nodes/analyzers/test_bundled_execution_surface.pysuitepasses: 154 passed (152 pre-existing + 2 new), confirming the asymmetry and no regressions.
ruff check,ruff format --check, andmypyare clean on both changed files.Signed-off-by: Udaya Tejas udayatejas2004@gmail.com