# Block .env and config-file probes in Express and Fastify

> Source: <https://dev.to/apitraffic/block-env-and-config-file-probes-in-express-and-fastify-2l6g>
> Published: 2026-11-06 12:18:00+00:00

Open the 404 log of any public API and you will find requests for files that were never meant to be served: `/.env`, `/.git/config`, `/.aws/credentials`. The list has grown. Scanners now ask for the config files of AI coding tools too, because those files hold API keys and MCP server tokens.

This post covers three things for a Node API: confirming none of those paths can return anything, answering them with a fast 404 before your app does any work, and counting the IPs behind them so repeat offenders get blocked.

Slobodan Manic [published 24 hours of his own site's logs](https://searchenginejournal.com/the-biggest-ai-crawler-on-my-website-was-hunting-for-credentials/585159) (Search Engine Journal, Aug 14). About 100 credential-hunting paths drew 1,028 requests and sent zero referrals. A few of the counts:

| Path | Requests | 
|---|---|
| `/.ssh/known_hosts` | 42 | 
| `/.mcp.json` | 30 | 
| `/.env.production` | 29 | 
| `/.continue/config.json` | 24 | 

Most of it carried a crawler user agent (CCBot), and the author says he couldn't verify whether it was the real crawler or someone borrowing the name. That matters for the fix: a user agent is whatever the client says it is, so don't build your blocking on it.

A fast 404 is pointless if the file is being served. Run the paths against production, and print the size as well as the status, because an SPA fallback can return `200` with your `index.html` for everything:

```
for p in /.env /.env.production /.git/config /.aws/credentials \
         /.ssh/known_hosts /.mcp.json /.continue/config.json; do
  curl -s -o /dev/null -w "%{http_code} %{size_download} $p\n" "https://api.example.com$p"
done
```

Anything other than a 404 or 403 gets a closer look. Then check how static files are served:

`public` folder, never at the project root or `process.cwd()`. That one line is how `.env` and `.git/` end up on the internet.`express.static('public', { dotfiles: 'deny' })``@fastify/static`, leave `serveDotFiles` off (it defaults to `false`).
Once nothing can leak, make the probes cheap. They should get a 404 before session lookup, body parsing, auth or a database query. Match on any path segment, since scanners also try `/api/.env` and `/app/.git/config`, and decode the path first so `/%2eenv` doesn't slip past:

``` js
// probe-guard.js
export const PROBE =
  /(?:^|\/)(?:\.env(?:\.[\w-]{1,32})?|\.git|\.aws|\.ssh|\.mcp\.json|\.continue|\.cursor|\.vscode|\.idea)(?:\/|$)/i;

export function decodedPath(url) {
  const path = url.split('?')[0];
  try { return decodeURIComponent(path); } catch { return path; }
}
```

Express: mount it first, above everything else.

``` python
import express from 'express';
import { PROBE, decodedPath } from './probe-guard.js';
import { recordProbe, banCheck } from './probe-counter.js';

const app = express();
app.use(banCheck);
app.use((req, res, next) => {
  if (!PROBE.test(decodedPath(req.originalUrl))) return next();
  recordProbe(req.ip, req.get('user-agent'), req.originalUrl);
  res.status(404).set('Cache-Control', 'no-store').end();
});
app.use(express.json());
// ...auth, routes
```

Fastify: [`onRequest`](https://fastify.dev/docs/latest/Reference/Hooks/) runs before the body is parsed, which is where this belongs.

``` js
fastify.addHook('onRequest', async (request, reply) => {
  if (!PROBE.test(decodedPath(request.url))) return;
  recordProbe(request.ip, request.headers['user-agent'], request.url);
  reply.code(404).header('cache-control', 'no-store').send();
  return reply;
});
```

`/.well-known/` doesn't match, so ACME challenges and `security.txt` keep working.

One probe is background noise. Twenty from the same address in a few minutes is someone working through a list. Log each hit with the IP, user agent and path, and deny an address that crosses a threshold:

```
// probe-counter.js (per process; use Redis or your edge for several instances)
const WINDOW_MS = 10 * 60_000, LIMIT = 5, BAN_MS = 60 * 60_000;
const hits = new Map();
const banned = new Map();

export function recordProbe(ip, ua, path) {
  const now = Date.now();
  const recent = (hits.get(ip) ?? []).filter((t) => now - t < WINDOW_MS);
  recent.push(now);
  hits.set(ip, recent);
  console.warn(JSON.stringify({ msg: 'probe', ip, ua, path, count: recent.length }));
  if (recent.length > LIMIT) banned.set(ip, now + BAN_MS);
}

export function banCheck(req, res, next) {
  const until = banned.get(req.ip);
  if (until && until > Date.now()) return res.status(403).end();
  if (until) banned.delete(req.ip);
  next();
}
```

The same `banCheck` logic works as a Fastify `onRequest` hook. Before you rely on it, check three things:

`req.ip` is only the real client when `trust proxy` names your actual proxies. Trust every hop and a scanner can write any address into `X-Forwarded-For`, including one of your customers'.` hits` on a timer, or the map grows with every scanner that has ever visited.
The paths change. AI tool configs are a recent addition, and the next tool's config folder will show up in your logs before it shows up in a blog post. Once a month, group your 404s by path, look at the top 50, and add anything that is clearly a credential or config file. Good candidates today: `.env` variants, `.git/`, `.aws/credentials`, `.ssh/`, `.mcp.json`, and editor or agent folders such as `.vscode/`, `.cursor/` and `.continue/`.

ApiTraffic's firewall runs inside your Node app through the SDK. Its managed rule set has Scanners & Probes and Sensitive Path Probing categories, and a custom rule can block a path regex on every server; policy changes reach connected servers without a deploy. Behavioral protection scores each IP over a window on signals that include 404 responses and requests to sensitive paths, and in Enforcing mode it pushes a temporary deny to every server running your app, so the counting above works across instances without Redis.

See what's hitting your API: [apitraffic.io](https://apitraffic.io/?utm_source=blog&utm_medium=content&utm_campaign=ai-era&utm_content=bf-20261106-block-env-probes-express)
