Block install-time code execution
Match npm lifecycle scripts, PyPI source-build hooks, Composer plugins, and RubyGems extensions and plugins with the install-scripts selector function.
The install-scripts selector function matches releases that can execute package-author code during installation or build. It supports npm, PyPI, Composer, and RubyGems and has no options.
For npm, PyPI, and RubyGems, it inspects the package artifact in the download and upload phases. For Composer, it reads plugin metadata and supports versions and download. Many legitimate packages match these conditions, so review log-only results before you enable blocking.
What matches
| Ecosystem | Matches | Does not match |
|---|---|---|
| Composer | A release with package type composer-plugin or the legacy composer-installer | Other package types, including the default library type |
| npm | Any of preinstall, install, postinstall declared in the package's package.json scripts | Other lifecycle scripts (prepare, postpublish, ...), scripts in dependencies of the package |
| PyPI | A source distribution (.tar.gz or .zip) with a setup.py at the root, or a root pyproject.toml whose [build-system] declares backend-path (an in-tree PEP 517 build backend) | Wheels (.whl), which install without running package code; sdists using an external build backend such as setuptools.build_meta without backend-path |
| RubyGems | A gem whose metadata declares extensions, which gem install builds by running the gem's own build code, or that ships a rubygems_plugin file at the root of one of its require paths, which RubyGems itself loads | A rubygems_plugin file anywhere else in the gem |
For PyPI, only files at the sdist root (or its single top-level directory) count; a setup.py under tests/ does not match.
Add the rule
The rule is not part of the default rule set; create it yourself. In the dashboard, open the firewall, go to Rules, and select Add rule. Choose Block packages from being downloaded on the intent step, then select Continue.
- Execution phase: Download for any supported ecosystem, or Versions for Composer when you want matching releases removed before resolution
- Selector function: Install-time code execution (no options)
- Effect: start with log only
For npm, PyPI, or RubyGems publishing, add the same rule on the Upload phase. Composer has no upload operation.
As JSON:
{
"id": "log-install-time-code-download",
"executionPhase": "download",
"selector": { "function": { "name": "install-scripts" } },
"ruleEffect": { "block": false, "log": true },
"description": "Log releases that can execute code at install time"
}To apply the JSON without the dashboard: export the config with bsfw firewall export, add the rule to the rules array, and import it back. See firewall import.
Unlike malware blocking, do not block on day one. Native modules, postinstall downloads, build hooks, Composer plugins, and gems with native extensions can all be legitimate. Run log-only, review the matches in logs, add exceptions for the packages you accept on the firewall's Exceptions tab, then switch the effect to block.
What a match looks like
The log entry's message names what was found. For npm:
{
"message": "Install scripts detected: preinstall, postinstall",
"scripts": ["preinstall", "postinstall"]
}For PyPI, the rule output also records the finding locations:
{
"message": "PyPI source-build hooks detected: setup.py",
"ecosystem": "pypi",
"artifactType": "sdist",
"findings": [
{ "kind": "setup.py", "path": "demo-1.0.0/setup.py" }
]
}A pyproject.toml backend-path finding additionally carries the declared backend and backendPath values.
For Composer, the rule output records the package type and any plugin classes declared in extra.class:
{
"message": "Composer plugin detected: Composer\\Installers\\Plugin",
"ecosystem": "composer",
"packageType": "composer-plugin",
"pluginClasses": ["Composer\\Installers\\Plugin"]
}For RubyGems, the rule output records the gem's platform and each finding:
{
"message": "RubyGems install-time code execution detected: extension",
"ecosystem": "rubygems",
"platform": "ruby",
"findings": [
{ "kind": "extension", "path": "ext/json/ext/parser/extconf.rb" }
]
}A finding's kind is extension or plugin. The firewall log records the version without its platform, so platform tells you which build matched: ruby is the source gem, which compiles its extension on install, while a prebuilt gem names its platform, such as x86_64-linux.
Inspection source
npm reads package.json from the package artifact. PyPI reads a root setup.py or pyproject.toml from an sdist. Composer reads type and extra.class from repository metadata, so it can match before the package file is downloaded. RubyGems reads the gem's metadata and file list from the .gem file; the RubyGems index carries neither, so it supports no versions-phase rule.
Verify
With the log-only rule in place, follow the install verification on the relevant ecosystem page, then inspect the matching entry in Logs. npm entries begin with Install scripts detected, PyPI entries with PyPI source-build hooks detected, Composer entries with Composer plugin detected, and RubyGems entries with RubyGems install-time code execution detected.
Related
- Concept: Rule evaluation
- Reference: Selector functions
- How-to: Manage exceptions, Block malware, Investigate a blocked install