npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sms-florin/mcp-ssrf-scan

v0.5.0

Published

A static scanner for MCP servers that catches the exact bug class behind most real MCP SSRF reports: a tool handler making an outbound request to a caller-controlled URL with no host validation.

Readme

mcp-ssrf-scan

A static scanner for MCP (Model Context Protocol) servers, built around one specific, common, real bug class: a tool handler making an outbound request to a URL the caller controls, with no check on where that URL actually points.

npx @sms-florin/mcp-ssrf-scan ./src

Why this exists

I found this exact bug in production, on my own server, before I ever thought about building a scanner for it. sms-florin is a real phone-number-rental API I run — a GOIP webhook route on it forwards SMS content from hardware gateways, and an early version of that route made an outbound call using a value that, with the wrong input, could have reached internal infrastructure instead of the intended destination. It got caught and fixed before it mattered, but it's the kind of bug that doesn't announce itself — the code runs, the tests pass, nothing looks wrong until someone points it at 169.254.169.254 or localhost:6379.

MCP tool handlers are the same shape, at scale: a function that takes caller-supplied arguments and, somewhere inside, makes a network call. BlueRock scanned 8,000+ public MCP servers and found 36.7% have unbounded URI fetch / SSRF flaws — this isn't a theoretical category, it's the single most common finding in the largest public scan of real MCP servers to date.

What it checks

  • SSRF (the headline check): a tool handler (server.tool(...) / setRequestHandler(...)) that passes a caller-controlled value into fetch/axios/http.request/https.request/got as the host — a caller value that only fills a path segment under a fixed or configured base URL is not reported — with no validation-looking identifier (allowlist, isPrivateIP, ssrf, ...) anywhere nearby.
  • Hardcoded secrets — Stripe/AWS/GitHub/Google key patterns, and a generic password/apiKey/token = "literal string" check (placeholders, env var names and values without a mix of letters and digits are ignored).
  • exec/Function injection — child_process.exec()/execSync() or new Function() called with an interpolated string, the shape a command-injection bug usually has. db.exec(), RegExp.exec() and values wrapped in a shell-escape helper are not reported.

Test, e2e, script, example and docs directories are skipped by default (run with --include-tests to scan them too). On a 2026-10-19 scan of 36 public MCP servers this cut 482 findings to 28.

  • Tool poisoning — a tool description that talks to the model instead of the user: hidden <IMPORTANT>-style tags, "ignore previous instructions", "don't tell the user", requests for key files or credentials, and invisible or bidirectional Unicode characters. An AI client reads tool descriptions as trusted instructions, so these are reported as HIGH.

Pin tool descriptions (rug-pull detection)

A tool description can be changed after you reviewed and approved it, and the client will follow the new text. Pin the current ones, commit the file, and fail CI when they change:

npx @sms-florin/mcp-ssrf-scan ./src --pin           # writes ./src/.mcp-tool-pins.json
npx @sms-florin/mcp-ssrf-scan ./src --check-pins    # in CI

--check-pins reports a changed description as HIGH (with the old and new hash), a new tool as MEDIUM and a removed one as INFO; after reviewing the change, re-run --pin to accept it. It runs fully locally: no account, no upload, no network. Whitespace-only edits don't change the pin. Use --pin-file path to keep the file elsewhere.

Limits: it reads string literals in JS/TS source (server.tool(...), registerTool(...), { name, description } entries). A description built at runtime, or defined in Python or another language, isn't seen.

What it is not

This is a regex-based heuristic scanner, not a real AST analyzer or a dynamic test. It has false positives (a validated call it doesn't recognize as validated) and false negatives (a vulnerable call it doesn't recognize as vulnerable, especially across files or through an abstraction layer). Treat a HIGH finding as "go read this function," not as a confirmed bug — and don't assume a clean scan means a server is safe.

CI usage

Exit code is 1 if any HIGH finding is reported, 0 otherwise:

- run: npx @sms-florin/mcp-ssrf-scan ./src

--json prints findings as JSON instead of the human-readable list.

--sarif prints SARIF 2.1.0 for code-scanning UIs. Secret findings are tagged OWASP MCP01 (Token Mismanagement & Secret Exposure) and exec findings MCP05 (Command Injection & Execution). The OWASP MCP Top 10 has no SSRF entry, so the SSRF rule carries no OWASP tag.

GitLab CI component

include:
  - component: gitlab.com/florin-builds/mcp-ssrf-scan/scan@~latest
    inputs:
      path: ./src

Inputs: stage (default test), path (default .), node_image (default node:22-alpine). The job fails on any HIGH finding and keeps mcp-ssrf-scan.sarif as an artifact.

mcp-ssrf-scan Cloud (optional, paid)

The CLI above is free and stays free. mcp-ssrf-scan Cloud ($15/month, up to 10 repos) adds what a CI gate alone can't: every run is uploaded and diffed against the previous scan of that repo, so the CLI prints exactly what's new since the last scan and what got fixed, and every scan stays browsable on a dashboard for 90 days.

# GitHub Actions
- run: npx @sms-florin/mcp-ssrf-scan@latest ./src --upload
  env:
    MCP_SSRF_SCAN_KEY: ${{ secrets.MCP_SSRF_SCAN_KEY }}
# GitLab CI (MCP_SSRF_SCAN_KEY as a masked CI/CD variable)
mcp-ssrf-scan:
  image: node:22
  script:
    - npx @sms-florin/mcp-ssrf-scan@latest ./src --upload

Repo, branch and commit come from the CI environment (or --repo owner/name). Only findings are uploaded — path, line, rule, message and the flagged line — never your source. A failed upload only prints a warning; the exit code is always decided by the findings alone.

License

MIT