How to Detect Install-Script Exfiltration

Intermediate 15 minutes Security Engineers Security & Policy

Block PhantomRaven-style dynamic install dependencies and obfuscated eval(atob(...)) payloads with the hasInstallScript and installScriptFetchesRemote conditions.

Overview

A package lifecycle script (preinstall, postinstall, prepare in npm; setup.py extensions in pip; *.gemspec extensions in RubyGems; Cargo.toml[package.build] + build.rs in Cargo; Composer lifecycle hooks) runs on the developer’s machine the moment the package is installed. An attacker who can publish a version of any dependency can use that hook to:

  • Pull a second-stage payload from a remote URL (PhantomRaven pattern)
  • Run curl | sh, wget | bash, subprocess.Popen, child_process.exec
  • Decode a base64 blob with eval(Buffer.from(…).toString()) or atob(…)
  • Reach ~/.aws/credentials, ~/.ssh/, CI secrets, environment variables

Chainsaw runs a static parse of the unpacked artifact before it resolves and classifies the result into install_script_kind{none, present, fetches_remote, eval_encoded}. Two policy conditions turn that classification into enforcement.

Prerequisites

  • Admin or Manager role in Chainsaw
  • Repositories proxying at least one of: npm, PyPI, RubyGems, Cargo, Composer

Step 1: Understand the Two Conditions

ConditionWhen it fires
hasInstallScriptAny lifecycle hook is present in the unpacked artifact manifest
installScriptFetchesRemoteThe hook body matches Chainsaw’s built-in fetch / eval detection regex

The regex covers curl, wget, fetch(, https.get / http.get, urllib(.request), requests.get, subprocess(.Popen|.run|.check_output|.call), child_process.exec(Sync), os.system, eval(Buffer.from, atob(, long base64-shaped blobs, and Function( constructors. A hit with only obfuscation markers (no fetch) sets install_script_kind = eval_encoded; the installScriptFetchesRemote condition matches both values.

Step 2: Roll Out in Monitor Mode

Create the first policy as a detection tripwire — not a block — so you see the inventory of packages in your organisation that already carry install scripts.

  1. Navigate to Policies → Create Policy
  2. Name: Monitor — hasInstallScript
  3. Condition: hasInstallScript = true
  4. Action: Warn (monitor)
  5. Scope: All repositories

After 7 days, check Violations and sort by package_metadata.install_script_kind. You’ll see a long tail of present (legitimate — most native bindings ship a postinstall gyp build), a short tail of fetches_remote, and occasional eval_encoded entries that deserve a closer look.

Step 3: Block the Remote-Fetch Class

Once you’ve triaged the monitor-mode signal, promote fetches_remote to a block:

  1. Name: Block — installScriptFetchesRemote
  2. Condition: installScriptFetchesRemote = true
  3. Action: Block
  4. Scope: Production repositories
  5. Precedence: High (above any general allowlist)
Some legitimate CLIs (Playwright, Electron, node-gyp rebuilds) may fetch platform binaries during install. Add targeted allowlist exceptions by package rather than relaxing the condition globally.

Step 4: Harden Against Obfuscation

Attackers commonly hide payloads as eval(Buffer.from(BASE64).toString()) or Function('…')(…). The eval_encoded classification catches these even when no URL appears in the hook body. Extend your policy to treat eval_encoded the same as fetches_remote:

conditions:
  installScriptFetchesRemote: true  # matches both fetches_remote and eval_encoded

Step 5: CLI Verification

The chainsaw CLI exposes the condition flag so you can dry-run a scan before shipping policy changes:

chainsaw pkg scan npm/@some-org/some-package \
  --version 1.4.2 \
  --with-install-scripts

Output includes install_script_kind, matched patterns, and the hook body excerpt that triggered the match.

Ecosystem Support

EcosystemhasInstallScriptinstallScriptFetchesRemote
npmpackage.json.scripts.{preinstall,install,postinstall,prepare,prepublish}
PyPIsetup.py + pyproject.toml[tool.setuptools]
RubyGems*.gemspec extensions + require_paths
CargoCargo.toml[package.build] + build.rs
Composercomposer.json.scripts
Maven / Gradle / NuGet / Go / HuggingFace / Swift / CocoaPods / Docker / APT / Yum / DNF❌ (no lifecycle-script concept)

Next Steps