SW-CFG-003: MCP server launched from an unpinned package
Last updated 7 November 2026
Also known as: unpinned MCP server package, rug pull (package form).
Explained in: MCP rug pull attacks
| Fact | Value |
|---|---|
| Severity | Medium |
| Checks | Configuration file |
| Malicious indicator | No |
| OWASP Agentic Top 10 | ASI04 |
| OWASP MCP Top 10 | MCP04 |
| NIST SP 800-53 | SR-3 (provisional mapping to SP 800-53 base controls) |
| First shipped in | Scanner 0.3.0 |
What it detects
The rule reads each server that has a command, whatever its transport, and judges the launcher. It knows npx, bunx, pnpx, pnpm dlx, yarn dlx, npm exec and bun x for Node packages, uvx, uv tool run and pipx run for Python packages, and docker run or podman run for images. One level of env, sh -c, bash -c, zsh -c or cmd /c in front is unwrapped first, and a script is split at semicolons, ampersands, pipes and line breaks, so sh -c "uvx tool@latest" is judged too.
What counts as pinned depends on the family. A Node package is pinned only by an exact version after its last at sign, such as pkg@1.4.2; a tag like @latest or @next, a range like ^2 and no version at all are not. A Python package is pinned by == or === with a release number, or by uv's pkg@1.2.3. An image is pinned by a @sha256: digest, never by a tag. A source taken from git is pinned only by a full 40-character commit, a Python archive URL by a #sha256= fragment, and a local path is exempt. A package named with -p, --package, --from, --with or --spec is judged like the first argument, and a requirements file given to uvx always fires.
Each unpinned launch is one finding on the server's line. The evidence names the launcher and the package, image or file and nothing else, because other arguments may hold a value that masking cannot recognise. A word that may be the value of an option the scanner does not know is shown as three stars.
Why it matters
A launcher without an exact version fetches whatever is current each time the agent starts the server. If the package changes hands, or a bad release is published, the next start runs it with the agent's permissions and with every file and token the server can reach. An exact pin turns the dependency into something that can be reviewed once and noticed when it changes. The scanner maps this rule to OWASP ASI04, Agentic Supply Chain Vulnerabilities, and MCP04, Software Supply Chain Attacks & Dependency Tampering, and to NIST SP 800-53 control SR-3, Supply Chain Controls and Processes.
Example
The server below starts through npx with the tag latest, which names no exact version:
.mcp.json
{
"mcpServers": {
"release-notes": {
"command": "npx",
"args": [
"-y",
"release-notes-mcp@latest"
]
}
}
}The evidence reads release-notes: npx release-notes-mcp@latest: the launcher and the package.
How to fix
Name an exact version: release-notes-mcp@1.4.2 for npx, tool==1.4.2 for uvx and pipx, or an image digest with @sha256:. Move to a newer release through a reviewed change, which a dependency bot can propose.
The private registry can admit a stdio server only when every package of its launch has an exact version (see the private registry guide), so the same pin also prepares the server for admission.
If it is a false positive
A launcher that runs a program kept in the same repository, by a relative or absolute path, is exempt and never reported. A server that you publish yourself and want to follow at its newest release matches by design: accept the finding with a note that says why the newest release is wanted, and keep the expiry short.
If the finding is not a problem in your case, accept it with a note: in the dashboard a manager or owner can accept a finding and the note is kept with it; with the scanner on its own, add it to the baseline file with a reason and an expiry date.