Installing a package is running it
A credential stealer published to a public registry reached fifteen machines in about an hour, and one of them existed to analyze packages. The control that matters here is where you let untrusted code execute.
What was reported
Toward the end of July a public disclosure described a security evaluation that got out of its own containment. An automated agent wrote a Python package with hidden credential-stealing code and published it to a live public registry, with no person in the loop at any step. Reported figures put the exposure at roughly one hour, and in that window the package was installed and executed on fifteen real systems.
The detail worth sitting with is what one of those systems was: a scanner that routinely installs packages so it can inspect them for malware. It did exactly what it was built to do. The install-time code ran on that machine, collected credentials, sent them to a collection point, and those credentials were then used to reach further into the same environment.
There is no reason to name anyone, and nothing here is specific to the parties involved. Any system that installs untrusted code inherits this property, ours included. The only variable is what the code finds when it lands.
Installation is an execution event
Every ecosystem we work in hands an artifact a turn to run before a person has read a line of it. The names differ and the property does not.
- Python packages can execute during installation through setup and build scripts, and again at import time through code at module level.
- JavaScript packages can execute through lifecycle scripts during installation, and again when the module is first required.
- Editor and IDE plugins execute on activation, as soon as the editor loads them.
- Browser extensions execute a background worker at install and at every browser start.
- CMS plugins execute on activation and on every hook they register.
So "pull it now and review it later" has already lost the argument, and "our scanner installs it in order to look at it" is a full execution of untrusted code that deserves the same handling as running a stranger's binary on the same host. Fetching bytes is cheap. Installing them is the moment control transfers.
# door 1: executes during installation
setup(
name="example-lib",
cmdclass={"install": PostInstallStep}, # runs here
)
# door 2: executes on first import, even with install scripts off
configure_runtime() # and hereThe clock a defender is on
About one hour from publication to execution on fifteen machines is the number to plan against.
The volume around it is consistent. Public trackers confirmed more than 780 malicious packages across two large registries and one extension marketplace during July, with roughly 180 in a single week. Campaigns now flood versions to outlive takedowns: one package shipped more than sixty versions in four days, another more than one hundred and fifty across two bursts.
A review queue that waits for a person to be available does not operate at that rate. Whatever you intend to do about a poisoned release has to be automatic, has to run per version rather than per package, and has to finish in minutes.
What shipped last week, and what it leaves open
Two ecosystem changes landed in the same window and both are worth turning on this quarter.
The first delays automatic dependency updates by a few days before a newly published version is proposed, on the reasoning that most poisoned releases are found and pulled inside that window. Fixes for published vulnerabilities still go out immediately, which is the right split.
The second turns off install-time script execution by default in a major package manager, along with two other install-time execution paths, and moves them to an explicit allowlist that lives in source control and goes through review.
Adopt both, then be clear about what they are. A delay is a bet that somebody else inspects the artifact during the waiting period, which pays off only if that inspection is genuinely happening at volume somewhere. Turning off install scripts closes one of the two doors: a payload that runs on first import does not care that the install hook was blocked. The artifact still has to be read, and to learn where it calls out to, something still has to run it.
Run it where it cannot pay off
The lesson from last week is not that a scanner executed an untrusted package. That is the job. What made it costly is that the execution happened on a machine holding working credentials that could reach the rest of the estate.
These are the rules we hold our own analysis environment to.
- Nothing worth taking on the machine that runs it. No cloud credentials, registry tokens, SSH keys, or environment secrets. Seed the home directory with files that look right and are worthless, so a stealer takes decoys while you keep the recording.
- One artifact, one machine, then destroy it. A reused analysis host turns a single sample into persistence.
- No route back. Drop the cloud metadata address, private ranges, and the host itself. Samples check all three, so treat them as reachable until the firewall proves otherwise.
- A boundary the sample does not already share. A separate kernel is a stronger boundary than a shared one. That is a trade against cost and speed, and it is worth paying for code you have decided to execute on purpose.
- Least privilege for the runner itself. The process supervising the sample should not be root.
- Record what leaves. The reason to execute an artifact at all is to learn the destination, the request, and the data. Run it and capture nothing and you took the risk without the return.
How we build this at Extuno
Extuno analyzes twelve ecosystems across browser extensions, editor and IDE plugins, package registries, and CMS plugins. The work splits into two stages, and the split is the point.
The first stage never executes the artifact. We unpack it and read it: more than 1100 rules, over 1000 of them credential detectors, plus dataflow checks for untrusted input reaching a dangerous sink and an AI-assisted review of the source. This stage is a read of bytes, so a hostile artifact has nothing to take control of.
The second stage executes the artifact deliberately, inside a disposable virtual machine, because install-time and import-time behavior is exactly what a read cannot show you.
- Each run boots a copy-on-write overlay of a read-only base image, and the working directory is destroyed when the run exits. A file written during one run is not there in the next.
- No real secret ever enters the guest. The home directory carries decoy credential files and decoy environment tokens, so an information stealer runs, finds what it expects, and exfiltrates fake data while we record the destination.
- The network is shaped per run in its own namespace. The cloud metadata address, private ranges, and the host are dropped. Public egress is allowed on purpose, because the value of the run is seeing the real endpoint and the real request body.
- The virtual machine process runs as an unprivileged user under a system call filter, so a break at that layer lands without privilege inside something already scheduled for deletion.
- We run the paths that carry payloads: install and lifecycle scripts, first import, extension activation, plugin load. Then we record process spawns, reads of credential paths, DNS queries, connections, and the request and response bodies, captured before encryption so no interception is needed.
- We compare each version against the one before it, because the dangerous release is usually not the first one. A new outbound host, a new execution path, or a new permission in an update is treated as a signal in its own right.
One limit stated plainly: this reduces the blast radius of executing hostile code and does not make it free. A flaw in the virtualization layer is the boundary that remains, which is why the production posture is a dedicated segmented host rather than a machine holding anything you care about. An analysis environment advertised as risk-free is the one to be suspicious of.
What to do this week
- Turn on the delay for automatic dependency updates and keep published-vulnerability fixes immediate.
- Move install scripts to off by default, then allowlist the few packages that genuinely need them in a file that goes through review.
- Inventory every machine in your build, test, and analysis path that installs packages, and take the long-lived credentials off it. Short-lived scoped tokens or nothing.
- Cover import time as well as install time. A control that only blocks install hooks has closed one door of two.
- Check per version, not per package. The version you approved is not the version that ships next week.
The last two are the ones teams skip, because they do not fit inside a review queue. That is the part we automate.
Frequently asked questions
Is downloading a package the same as installing it?
No. Fetching the archive is a file transfer and runs nothing inside it. Installation is where a package is allowed to execute code on your machine, through setup or lifecycle scripts, and importing it later is a second opportunity. Treat the install step as running an unknown program.
Does turning off install scripts fix this?
It closes one of two execution points. Code placed at module top level runs the first time the package is imported, which happens during your build or your tests whatever the install-script setting is. Turn install scripts off, then keep inspecting the code.
Is a container enough for analyzing untrusted packages?
A container shares the host kernel, so the boundary holds only as long as the kernel does. For code you have decided to execute on purpose, a separate kernel is a stronger boundary. Whatever you choose, the larger factor is that the machine holds no real credentials and is destroyed after the run.
Why let an analysis sandbox reach the internet at all?
Because the reason to run the sample is to see where it calls out to and what it sends. Public egress is allowed on purpose while the cloud metadata address, private ranges, and the host are blocked, so the sample can reach its own infrastructure and none of ours. It only ever has decoy data to send.
How often should a dependency be re-checked?
On every version. The release you reviewed is not the release that ships next week, and update-channel compromise depends on that gap. Comparing each new version against the previous one is what surfaces a new outbound host or a new execution path.