Auditing Your MCP Server for Internet Exposure, Credential Leaks, and Unauthenticated Endpoints

Auditing Your MCP Server for Internet Exposure, Credential Leaks, and Unauthenticated Endpoints

pr0h0
mcpcybersecurityllm-securitydevsecops
AI Usage (80%)

Why this scan wave matters right now

The reporting behind this post points to active scanning for three things at once: exposed MCP servers, AI assistant credentials, and unauthenticated LLM endpoints. That combination matters because it matches a pattern I keep seeing in real environments: an AI feature starts as a local tool, grows into a network service, and then gets a token bolted on later.

My view is simple: this is not “just more bot noise.” It is a sign that AI integrations now sit inside the public attack surface, and they are often built with less discipline than the rest of the app.

📝

What I can confirm from the reporting: scanners are looking for exposed MCP servers, AI assistant credentials, and unauthenticated LLM endpoints. What I did not verify: the scanner operator, exact scale, or whether every target was misconfigured in the same way.

What the reporting says the scanners are looking for

The scan pattern is broad, but it hangs together:

  • MCP servers that answer from the network when the team expected local-only access
  • assistant-related secrets in environment files, config snippets, logs, or shell history
  • LLM or agent endpoints that answer without authentication, even if they were meant for internal use only

That mix matters because the first issue gives an attacker reach, the second gives them credentials, and the third gives them something to do with either the credentials or the exposed tool surface.

Why MCP servers and LLM endpoints are unusually exposed by default

MCP servers are often built for developer convenience first. That usually means:

  • local bind addresses during development
  • no auth in the first working version
  • broad tool descriptions so clients can discover capabilities quickly
  • environment variables or config files that are easy to copy into CI, Docker, or a VM

The catch is that “easy to copy” also means “easy to publish by accident.” Once the service moves from localhost to 0.0.0.0, the threat model changes immediately. At that point, anything that exposes manifests, tool schemas, logs, or debug routes can become useful to a scanner.

Start with an inventory, not a fix

I would not start by rewriting auth middleware. I would start by finding every place the service exists.

Find every MCP server, sidecar, and experimental endpoint

On a small team, inventory is usually a mix of shell, container, and cluster checks. A quick pass might look like this:

## Find listening processes on a host
ss -ltnp

## Inspect local services
lsof -i -P -n | grep LISTEN

## Find running containers and published ports
docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Image}}'

## In Kubernetes, look for services and ingress
kubectl get svc,ingress -A

If you have several repos, also search for likely transport and manifest routes in code and deployment manifests.

git grep -nE 'mcp|tool|manifest|health|stream|sse|0\.0\.0\.0'

This is not exhaustive, but it gives you a map. Without the map, you will fix one endpoint and leave three others exposed.

Separate local-only tools from anything reachable over the network

I treat these as two different classes:

ClassExpected reachabilityRisk profile
Local-only dev toolLoopback or Unix socket onlyLow if it stays local
Internal servicePrivate network or VPNMedium if auth is weak
Remote-facing servicePublic or reverse-proxiedHigh, even if “read only”

The mistake is to assume that a service is safe because it is “just for developers.” If it listens on a routable interface, it is no longer just for developers.

Record the auth mode, transport, and host binding for each service

For each endpoint, write down three things:

  • transport: HTTP, SSE, WebSocket, stdio bridge, or something else
  • host binding: 127.0.0.1, private subnet, 0.0.0.0, or a public ingress
  • auth mode: none, bearer token, session cookie, mTLS, VPN-only, or proxy-authenticated

That record becomes your baseline. If you cannot answer those three questions, you do not really know what you are shipping.

Test for internet exposure from the outside in

The outside-in test is the one scanners are already doing. You should do it yourself before they do.

Check whether the server is bound to 0.0.0.0, public IPs, or a reverse proxy

A local bind usually means you intended private access. A public bind or ingress means you need real controls.

Example:

curl -i http://127.0.0.1:3000/health
curl -i http://YOUR_PUBLIC_IP:3000/health

If the first request works and the second fails, that is usually what you want. If both work, the service is reachable from the network. If the second works from the internet, assume scanners will find it quickly.

Verify whether health, manifest, or tool routes answer without authentication

Do not stop at the health check. A lot of teams protect the action route but leave discovery routes open.

A simple audit pattern is:

BASE="https://mcp.example.com"

curl -i "$BASE/health"
curl -i "$BASE/.well-known/mcp.json"
curl -i "$BASE/tools"
curl -i "$BASE/mcp"

Your exact paths will vary. The point is to test the same classes of routes scanners usually probe: health, manifest, and tool surface.

Here is the difference I want to see:

RequestGood responseBad response
Health401 or 403 if private200 OK with no auth
ManifestAuth required or no public routeTool list returned openly
Tool callRejected without authState change or upstream call succeeds

Example protected response:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="internal"
Content-Type: application/json

{"error":"authentication required"}

Example exposed response:

HTTP/1.1 200 OK
Content-Type: application/json

{"tools":[{"name":"repo_search"},{"name":"deploy"}]}

That second response is not harmless just because it is “only metadata.” It tells an attacker what the service can do and where to focus next.

Show the difference between expected local access and actual public reachability

I like to test both from the same host:

## expected local-only
curl -i http://127.0.0.1:3000/health

## actual network path
curl -i http://$(hostname -I | awk '{print $1}'):3000/health

If loopback works and the network path also works, you have confirmed public or at least LAN reachability. If that service was meant to stay local, treat it as a configuration bug, not an abstract risk.

Look for credential leaks tied to AI assistants and MCP tooling

The scanner wave also reportedly targets assistant credentials. That matters because leaked keys turn a reachability bug into account abuse.

Common leak points in env files, config snippets, logs, and shell history

The usual places are boring, which is exactly why they get missed:

  • .env, .env.local, and CI environment dumps
  • example config files checked into repos
  • application logs that print startup configuration
  • notebook cells and pasted terminal transcripts
  • shell history from export TOKEN=... or curl -H "Authorization: Bearer ..."

A few safe searches help:

## Search the repo for likely secret patterns
git grep -nE 'sk-[A-Za-z0-9]{16,}|Bearer [A-Za-z0-9._-]+|api[_-]?key|token'

## Check recent shell history carefully
tail -n 50 ~/.bash_history
tail -n 50 ~/.zsh_history

How to confirm a secret is real without pasting it into the article or sharing it broadly

Do not post the token. Do not spray it at random endpoints. Confirm it locally and quietly.

Useful checks:

  1. Compare the suspected value against the provider’s token format.
  2. Check whether the token is currently active in the provider dashboard.
  3. Use a throwaway validation request from your own machine.
  4. If the token may have leaked, revoke first and verify the revoke succeeded.

If you need to prove impact internally, capture only the minimum evidence:

  • token prefix or last four characters
  • account or project name
  • timestamp
  • the status code or error text, redacted

That is enough to show the secret was real without spreading it further.

Why assistant tokens and service keys deserve separate handling

I would not lump these together.

  • assistant tokens often grant access to user-facing workflows or chat agents
  • service keys often unlock backend APIs, tool calls, or deployment actions

If one leaks, you may not be able to reuse the same rotation path for the other. In practice, I rotate them separately, log them separately, and scope them separately. A shared secret pool is how a small leak turns into a broad incident.

⚠️

If an assistant token or service key has been exposed, rotate it as an incident response task, not as cleanup. Assume copies already exist in logs, terminals, and pasted snippets.

Probe unauthenticated endpoints for real impact, not just presence

An open route is not always exploitable, but it is always worth classifying.

Identify which endpoints disclose capabilities, schemas, or tool names

Start by asking what the endpoint reveals before you ask what it can change.

Examples of useful questions:

  • Does it list tools or actions?
  • Does it return schema details or parameter names?
  • Does it disclose upstream hosts, model names, or connector IDs?
  • Does it echo config values, build metadata, or debug flags?

Even if the route does nothing dangerous by itself, the metadata can help an attacker chain the next step.

Check whether unauthenticated requests can trigger state changes or upstream calls

This is the line I care about most. Metadata exposure is one problem. Execution is another.

A safe test pattern is to use a disposable account or a test environment and ask:

  • can I trigger a read-only call?
  • can I cause a write or deploy action?
  • can I make the server call an upstream API on my behalf?

If the answer is yes without auth, the impact is no longer theoretical.

Explain the difference between metadata exposure and tool execution risk

Exposure typeWhat leaksWhy it matters
Metadata onlyTool names, schemas, route namesHelps attackers target the next step
Read-only accessData, records, statusCan still expose sensitive internal info
Write accessChanges, jobs, tickets, deploysDirect business impact
Delegated upstream callsThird-party API usage or state changeCan amplify abuse outside your network

My position: teams often underestimate metadata exposure. I would not dismiss it, but I would prioritize any unauthenticated route that can actually do something.

Practical hardening steps that actually shrink exposure

Bind internal services to loopback or private interfaces by default

If a service is meant to be local, make local the default. Use 127.0.0.1, not 0.0.0.0. In containers, be explicit about published ports and network policy. In Kubernetes, keep internal services off public ingress.

Put authentication in front of every remote MCP path

No remote path should be unauthenticated by accident. That includes:

  • manifest routes
  • health routes if they reveal too much
  • debug routes
  • tool execution routes
  • any reverse-proxied internal admin surface

If the path is reachable remotely, it needs auth. “Read only” is not a substitute.

Add allowlists, mTLS, or VPN access where public reach is not required

If the service is only for employees or trusted clients, do not publish it to the open internet. A few good options are:

  • IP allowlists for tightly controlled environments
  • mTLS for service-to-service access
  • VPN access for internal-only tools
  • proxy authentication in front of the app

Pick one that matches the use case, then make sure the app still rejects direct access.

Rotate any secret that may have been exposed and invalidate old sessions

If a key may have leaked, assume it did. Rotate it. Then invalidate sessions, refresh tokens, and any downstream credentials that were derived from it.

The common mistake is rotating the obvious token while leaving old sessions alive in caches, proxies, or long-lived jobs.

Verify the fix with the same tests the scanner would use

Re-run the outside-in curl checks and document the blocked responses

After the fix, repeat the same requests:

curl -i http://YOUR_HOST:3000/health
curl -i https://mcp.example.com/.well-known/mcp.json
curl -i https://mcp.example.com/tools

The output I want to see is a denial, not a “mostly hidden” success.

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"error":"authentication required"}

If you get a 200 from anywhere public, keep digging.

Confirm logs, alerts, and rate limits are firing on probe traffic

A good hardening pass should also leave traces:

  • auth failures should hit logs
  • repeated probes should trigger alerts or rate limits
  • unexpected manifest discovery should be visible in access logs

If the system silently absorbs probe traffic, you may have fixed reachability but lost observability.

Note what you did not test and where manual review is still needed

I would be explicit about gaps:

  • I did not test every auth provider or proxy path.
  • I did not validate third-party SaaS connectors.
  • I did not inspect downstream tool permissions in this article.
  • I did not attempt an authenticated abuse path.

That keeps the review honest. It also tells the next reviewer where to spend time.

What I would prioritize first in a real deployment

Fix public reachability before tuning the tool surface

If an MCP server or LLM endpoint is reachable from the internet, I would fix that before I polish prompts, tool descriptions, or schemas. Reachability is the widest opening. Close it first.

Treat leaked credentials as an incident, not just a cleanup task

A leaked assistant token is already a security event. The right response is revocation, session invalidation, log review, and a quick look for misuse. Cleanup comes after containment.

Close unauthenticated endpoints even if they look read-only

I do not buy the argument that a manifest or health route is harmless if it is public. Discovery helps attackers. In practice, I would close unauthenticated endpoints unless I had a strong reason not to.

Further reading

Share this post

More posts

Comments