cd /news/ai-crawlers/block-env-and-config-file-probes-in-… · home › topics › ai-crawlers › article
[ARTICLE · art-165307] src=dev.to ↗ pub= topic=ai-crawlers verified=true sentiment=· neutral

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

A developer published a guide for Node.js APIs showing how to block credential-hunting probes for files like .env, .git/config, .aws/credentials and AI coding tool configs such as .mcp.json and .continue/config.json. The approach verifies sensitive paths return 404 or 403 in production, serves static files only from a public folder with dotfiles denied, and mounts a regex-based probe guard before body parsing, auth or database work in Express and Fastify. Hits are logged by IP, user agent and path, with repeat offenders banned after crossing a threshold, since user agents like CCBot cannot be trusted.

by read4 min views1 publishedNov 6, 2026

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 (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:

// 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.

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 runs before the body is parsed, which is where this belongs.

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

── more in #ai-crawlers 4 stories · sorted by recency
── more on @express 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/block-env-and-config…] indexed:0 read:4min 2026-11-06 · —