WAF rules for AI-agent credential and dev-server probes, the secure way
Your access log is full of requests for /.env, /.mcp.json and /@fs/etc/passwd. None of those files should exist on a production site, and the day one of them does, the scanner will be first to know.
The short answer
Keep OWASP CRS, and add two edge rules that answer 404 before the request reaches the app: one for dev-server internals (/@fs/, /@vite/, /@id/, node_modules/.vite) and one for secret and agent config files (.env*, .git/, .aws/, .ssh/, .npmrc, .mcp.json, .cursor/, .claude/). CRS alone misses several of them.
On this page
What goes wrong
Two kinds of files leak secrets through web servers. The first kind has
always been there: .env files, .git/, cloud credentials, .npmrc
tokens. The second kind is newer: AI coding tools keep their settings in the
project directory, for example .mcp.json, .cursor/mcp.json and
.claude/settings.json, and those files can hold API keys and MCP server
tokens. Any of them ends up on the web the same way: a build step that
copies the whole project directory into the web root.
Dev servers are the other hole. The Vite dev server serves files by absolute
path under /@fs/. It is meant for a laptop, but it gets exposed with
--host, in a preview environment, or in a container that runs npm run dev. Vite has had several bypasses of its file restrictions, including
CVE-2025-30208, where adding ?raw?? to a /@fs/ URL returned files that
should have been denied.
The OWASP Core Rule Set catches many of these requests, but not all. It looks for known restricted file names. It does not know dev-server paths, and its list of restricted files does not include every AI tool's config.
What the docs say
Blocklist for sensitive files being restricted to be served by Vite dev server.
Source: Vite docs, server.fs.deny
Locally preview the production build. Do not use this as a production server as it's not designed for it.
Source: Vite docs, CLI, vite preview
Adding
?raw??or?import&raw??to the URL bypasses this limitation and returns the file content if it exists.
Source: NVD, CVE-2025-30208
Vite's deny list protects the dev server from itself, when it works. It does nothing for a dev server that should not be reachable at all, and it does not cover files outside its list, such as AI tool configs. That job belongs at the edge.
The secure configuration
Two Coraza rules, placed after Include @crs-setup-conf and before the CRS
rules, so they decide first. Each line is one directives_map entry in
coraza-proxy-wasm.
Include @recommended-conf
SecRuleEngine On
Include @crs-setup-conf
# 1. Dev-server internals have no business at a production edge.
# Normalize first so /%40fs/, //@fs/ and /./@fs/ match too.
SecRule REQUEST_FILENAME "@rx ^/(?:@fs|@vite|@id|@react-refresh|__vite_ping|node_modules/\.vite)(?:/|$)" \
"id:10200,phase:1,deny,status:404,log,t:none,t:urlDecodeUni,t:normalizePath,t:lowercase,msg:'Dev server path at the edge',tag:'probe/dev-server'"
# 2. Secret files and AI coding-agent configs, in any directory.
SecRule REQUEST_FILENAME "@rx (?:^|/)(?:\.env(?:\.[a-z0-9_.-]+)?|\.git/.*|\.aws/.*|\.ssh/.*|\.docker/config\.json|\.npmrc|\.pypirc|\.netrc|\.mcp\.json|\.cursor/.*|\.claude/.*|\.vscode/sftp\.json)$" \
"id:10210,phase:1,deny,status:404,log,t:none,t:urlDecodeUni,t:normalizePath,t:lowercase,msg:'Secret or agent config file probe',tag:'probe/secret-file'"
Include @owasp_crs/*.confWhy these choices:
REQUEST_FILENAMEis the path without the query string, so?raw??tricks do not change the match.t:urlDecodeUni,t:normalizePath,t:lowercasedefeats encoded and mixed-case variants like/%2e%65nvor/.ENV.status:404tells a scanner nothing is there. A 403 confirms the path is interesting.phase:1decides on the request line, before any body is read.
Then fix the cause: build artifacts must never contain these files. Add them
to .dockerignore, and serve only the build output directory.
Prove it
The test in secure-tests/waf-rules-ai-agent-dev-server-probes/run.sh sends
17 requests through Coraza with CRS 4.14.0 at paranoia level 1, first alone,
then with the two rules.
== 1. OWASP CRS 4.14, paranoia level 1, no custom rules
GET / -> 200
GET /static/app.js -> 200
GET /docs/environment-variables -> 200
GET /.env -> 403
GET /.env.production -> 403
GET /app/.env.local -> 403
GET /%2e%65nv -> 403
GET /.git/config -> 403
GET /.aws/credentials -> 403
GET /.mcp.json -> 200
GET /.cursor/mcp.json -> 200
GET /.claude/settings.json -> 200
GET /.vscode/sftp.json -> 403
GET /@fs/etc/passwd -> 200
GET /@fs/app/.env?raw?? -> 403
GET /@vite/client -> 200
GET /node_modules/.vite/deps/react.js -> 200
== 2. The same plus two custom rules (dev-server paths, secret files)
GET / -> 200
GET /static/app.js -> 200
GET /docs/environment-variables -> 200
GET /.env -> 404
GET /.env.production -> 404
GET /app/.env.local -> 404
GET /%2e%65nv -> 404
GET /.git/config -> 404
GET /.aws/credentials -> 404
GET /.mcp.json -> 404
GET /.cursor/mcp.json -> 404
GET /.claude/settings.json -> 404
GET /.vscode/sftp.json -> 404
GET /@fs/etc/passwd -> 404
GET /@fs/app/.env?raw?? -> 404
GET /@vite/client -> 404
GET /node_modules/.vite/deps/react.js -> 404CRS alone lets through the three AI tool configs and every dev-server path,
including /@fs/etc/passwd: CRS checks its list of operating system files
(rule 930120) against cookies and arguments, not the path, and its
restricted-files list (rule 930130) does not contain those AI tool files. With the two rules, every probe gets a 404
and the three normal requests, including a docs page with "environment" in
its name, still get a 200.
Mistakes people make
Blocking with a pattern that also matches your content
A rule on env anywhere in the path blocks /docs/environment-variables.
Anchor on the file name ((?:^|/)\.env) and test with your real URLs.
Matching the raw URI instead of the normalized path
REQUEST_URI includes the query string and the original encoding. Match
REQUEST_FILENAME with decoding and normalization, or /%2e%65nv walks
past.
Treating the WAF as the fix
The rule hides a mistake; it does not remove it. If a .env file is in your
web root, rotate every secret in it and fix the build that put it there.
Letting a dev server reach the internet
A preview environment that runs vite or next dev is a dev server with a
public address. Put previews behind authentication, or build and serve
static output there too.
Answering 403
A 403 on /.env tells a scanner the path is guarded, which is often a hint
that it exists behind the guard. Return 404.
Checklist
- Add the dev-server rule (10200) and the secret-file rule (10210) before the CRS rules.
- Match
REQUEST_FILENAMEwithurlDecodeUni,normalizePathandlowercase. - Answer 404, not 403.
- Test the rules against a list of your real URLs for false positives.
- Keep
.env*,.git,.mcp.json,.cursor/,.claude/out of images with.dockerignore. - Never expose a dev server; build and serve static output in previews.
- Feed rule 10200 and 10210 hits into your ban list; see WAF auto-ban.
The scanners read the same changelogs you do. Two rules make sure they find a 404 instead of your API keys.
H2-CPQE
Learn it on a live range
WAF, rate limiting and DDoS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome