npm Lifecycle Entrypoint Analysis¶
Safety boundary: This workbook performs static analysis of sanitized excerpts, fake fixtures, and derived public metadata. It does not execute attacker code, inspect the analyst's environment, or contact any network.
Code Path¶
root npm install
-> npm lifecycle scheduler
-> prepare
-> start /b node server || nohup node server &
-> node server
prepare is lifecycle code, not metadata. During a normal install npm may hand
that string to a platform shell. The user therefore crossed a code-execution
boundary by installing the root project, before explicitly starting the app.
import json
from pathlib import Path
manifest = json.loads(Path("../fixtures/fake-package-json.json").read_text())
lifecycle_names = {"preinstall", "install", "postinstall", "prepare"}
lifecycle = [
{"hook": name, "command": command}
for name, command in manifest.get("scripts", {}).items()
if name in lifecycle_names
]
lifecycle
Shell Semantics, Parsed as Data¶
The preserved command was:
start /b node server || nohup node server &
On Windows, start /b attempts a background launch. On Linux/NixOS, that
Windows-oriented command fails, so || selects the Unix-like fallback.
nohup reduces coupling to the terminal and & backgrounds the child.
Nothing below executes the command; it is tokenized from an inert fixture.
command = manifest["scripts"]["prepare"]
segments = [
{
"segment": "start /b node server",
"meaning": "Windows background launch attempt",
"risk": "hidden runtime",
"classification": ["installer-triggered", "backgrounding", "server launch"],
},
{
"segment": "||",
"meaning": "run fallback if the first branch fails",
"risk": "cross-platform resilience",
"classification": ["cross-platform fallback", "suspicious"],
},
{
"segment": "nohup node server &",
"meaning": "Unix-like background launch",
"risk": "detached process",
"classification": ["backgrounding", "server launch", "suspicious"],
},
]
{"command_matches_fixture": command == "start /b node server || nohup node server &",
"segments": segments}
| Segment | Meaning | Risk |
|---|---|---|
| start /b node server | Windows background launch attempt | hidden runtime |
| || | fallback if the prior command fails | cross-platform resilience |
| nohup node server & | Linux/NixOS background launch | detached process |
| node server | server entrypoint | imports the malicious chain |
process_ancestry = [
{"depth": 0, "process": "npm", "cause": "user ran root install"},
{"depth": 1, "process": "platform shell", "cause": "npm invoked prepare"},
{"depth": 2, "process": "nohup", "cause": "fallback branch on Linux/NixOS"},
{"depth": 3, "process": "node server", "cause": "background server entrypoint"},
]
process_ancestry
Why This Is Useful to an Attacker¶
The command gains three properties with one manifest line: automatic execution,
cross-platform fallback, and a backgrounded process likely to outlive the
terminal session. The application does not need to serve a legitimate request.
Node only needs to evaluate server.js and its imports.
Evidence: preserved pre-containment manifest, victim account of the root
install, and reconstructed nohup runtime evidence.
Strongest supported claim: the manifest supplied an automatic install-time
path to a backgrounded node server; runtime evidence corroborates that this
path reached the downloaded stage.
Not proven by the manifest alone: the exact process lifetime or every child PID. Process accounting, audit logs, or full shell history would strengthen it.
Prevention: review manifests and use an isolated, secret-free host with scripts disabled during first inspection.
Detection: alert when npm is parent or ancestor of nohup, a shell
background operator, or an unexpected long-lived Node process.