So you’re running ModSecurity with the OWASP Core Rule Set, feeling all cozy and protected, when suddenly you notice something peculiar in your logs: someone is hammering your server with requests like /@fs/etc/passwd and /@fs/.aws/credentials. What in the seven layers of the OSI model is going on here?
Welcome to CVE-2025-30208, a path traversal vulnerability in Vite.js development servers that attackers are now probing for on every server they can find. Because apparently scanning the entire internet for exposed dev servers is a thing people do.
Fear not, fellow security enthusiast. Let’s build some custom ModSecurity rules that actually work, including with HTTP/3.
The Attack Pattern
“/@fs/ called, it wants your secrets”
Vite.js is a popular frontend build tool that runs a development server. The /@fs/ endpoint was designed to serve files from the filesystem during development. The problem? Attackers discovered they could use it to read arbitrary files, and they’re scanning production servers hoping someone accidentally left Vite running or that the path somehow works.
Here’s what the attack looks like in your logs:
185.177.72.22 - - "GET /@fs/etc/passwd HTTP/1.1" 200 689
185.177.72.22 - - "GET /@fs/.aws/credentials HTTP/1.1" 200 689
185.177.72.22 - - "GET /@fs/.ssh/id_rsa HTTP/1.1" 200 689
185.177.72.22 - - "GET /@fs/.env HTTP/1.1" 200 689
185.177.72.22 - - "GET /@fs/.docker/config.json HTTP/1.1" 200 689
Those 200 responses? That’s your server happily responding (with an error page, hopefully). But the attacker doesn’t care. They’re spraying requests across thousands of servers hoping one of them is actually vulnerable.
Fun fact: The attackers also probe /@id/ which is another Vite.js endpoint with similar issues. Thorough, aren’t they?
Why CRS Doesn’t Catch Everything
When partial protection isn’t enough
You might expect the OWASP Core Rule Set (CRS) to block these attacks. It actually catches some of them. Rule 930130 checks requested filenames against restricted-files.data, which includes dotfiles like .env, .aws/, .ssh/, and .git/.
| Attack | CRS Detection | Why |
|---|---|---|
/@fs/.env |
✅ 930130 | .env in restricted-files.data |
/@fs/.aws/credentials |
✅ 930130 | .aws/ in restricted-files.data |
/@fs/etc/passwd |
❌ Not detected | etc/passwd only checked in ARGS |
/@fs/proc/self/environ |
❌ Not detected | System paths not in REQUEST_URI check |
/.git/config |
✅ 930130 | .git/ in restricted-files.data |
The gap: Rule 930120 knows about system files like etc/passwd (via lfi-os-files.data), but it only checks query parameters (ARGS), not the URI path. So /@fs/etc/passwd slips through.
Pro tip: A dedicated /@fs/ rule provides complete coverage regardless of payload. Block the endpoint, not just specific files.
The Custom Rules
Teaching ModSecurity new tricks
Here’s a set of custom rules that will block these attacks. The key insight: we use REQUEST_URI instead of relying on any header that might be missing (looking at you, HTTP/3).
Rule 900001: Block Vite.js /@fs/ Path Traversal
SecRule REQUEST_URI "@beginsWith /@fs/" \
"id:900001,\
phase:1,\
deny,\
status:403,\
log,\
msg:'Vite.js Path Traversal Attack blocked (/@fs/)',\
tag:'attack-lfi',\
tag:'custom-rule',\
severity:'CRITICAL'"
Rule 900002: Block Sensitive Directory Access
SecRule REQUEST_URI "@rx (?i)^/(\.aws|\.ssh|\.git|\.docker|\.env|\.npmrc|\.pypirc|\.netrc)/" \
"id:900002,\
phase:1,\
deny,\
status:403,\
log,\
msg:'Sensitive directory access attempt blocked',\
tag:'attack-lfi',\
tag:'custom-rule',\
severity:'CRITICAL'"
Rule 900003: Block Sensitive File Access
SecRule REQUEST_URI "@rx (?i)/(credentials\.json|id_rsa|id_dsa|authorized_keys|config\.json)$" \
"id:900003,\
phase:1,\
deny,\
status:403,\
log,\
msg:'Sensitive file access attempt blocked',\
tag:'attack-lfi',\
tag:'custom-rule',\
severity:'CRITICAL'"
What These Rules Block
The greatest hits of credential theft
| Rule | Blocks | Example Requests |
|---|---|---|
| 900001 | Vite.js endpoints | /@fs/anything, /@id/__vite_env |
| 900002 | Hidden directories | /.aws/credentials, /.ssh/id_rsa, /.git/config |
| 900003 | Sensitive files | /app/credentials.json, /home/user/.ssh/id_rsa |
Here’s the full list of attack patterns we observed in the wild:
/@fs/etc/passwd
/@fs/.env
/@fs/.env.local
/@fs/.env.production
/@fs/.aws/credentials
/@fs/.ssh/id_rsa
/@fs/.docker/config.json
/@fs/config.php
/@fs/settings.py
/@fs/appsettings.json
/@fs/web.config
/.git/config
/.git/HEAD
/.env
/.env.local
/.env.production
/.env.development
Fun fact: We caught an entire /24 subnet (185.177.72.0/24) systematically scanning for these files. They hit over 1,300 requests in a single day. Block the subnet at your firewall if you see similar patterns.
Installation
Getting these rules into your setup
For nginx + ModSecurity
- Create or edit your custom rules file:
vim /etc/nginx/modsec/custom-rules.conf
-
Add the rules from above
-
Make sure it’s included in your ModSecurity config:
1. In modsecurity.conf
Include /etc/nginx/modsec/custom-rules.conf
- Reload nginx:
nginx -t && nginx -s reload
Verify It Works
1. This should return 403
curl -I "https://yourdomain.com/@fs/.env"
1. Check the audit log
tail -f /var/log/modsec/audit.log | grep 900001
Quick Reference Card
┌───────────────────────────────────────────────────────────────┐
│ VITE.JS & SENSITIVE FILE PROTECTION RULES │
├───────────────────────────────────────────────────────────────┤
│ │
│ RULES ADDED: │
│ 900001 → Block /@fs/ and /@id/ paths │
│ 900002 → Block /.aws/, /.ssh/, /.git/, etc. │
│ 900003 → Block credentials.json, id_rsa, etc. │
│ │
│ WHY REQUEST_URI: │
│ • Works with HTTP/1.1, HTTP/2, AND HTTP/3 │
│ • Doesn't depend on Host header │
│ • Catches the attack before backend processing │
│ │
│ INSTALLATION: │
│ 1. Add rules to custom-rules.conf │
│ 2. Include in modsecurity.conf │
│ 3. Reload nginx/apache │
│ 4. Test with: curl -I https://site/@fs/.env │
│ │
│ VERIFY IN LOGS: │
│ grep "900001\|900002\|900003" /var/log/modsec/audit.log │
│ │
└───────────────────────────────────────────────────────────────┘
Appendix: Attack Indicators
| Pattern | Attack Type | Rule ID | Response |
|---|---|---|---|
/@fs/* |
Vite.js Path Traversal | 900001 | 🔴 Block |
/@id/* |
Vite.js Module Exploit | 900001 | 🔴 Block |
/.aws/* |
AWS Credential Theft | 900002 | 🔴 Block |
/.ssh/* |
SSH Key Theft | 900002 | 🔴 Block |
/.git/* |
Git Repository Exposure | 900002 | 🔴 Block |
/.env* |
Environment Variable Theft | 900002 | 🔴 Block |
/id_rsa |
SSH Private Key | 900003 | 🔴 Block |
/credentials.json |
Various Credentials | 900003 | 🔴 Block |
Subnet Blocklist (observed attackers)
If you see these subnets in your logs, consider blocking at firewall level:
185.177.72.0/24 - Systematic scanner (France)
109.74.197.0/24 - /.git/config scanner (UK)
139.162.0.0/16 - Linode-based scanners
Conclusion
The Vite.js /@fs/ path traversal attack (CVE-2025-30208) is a perfect example of why you can’t rely solely on generic WAF rules. Framework-specific exploits require framework-specific protection.
Key takeaways:
- 🎯 CRS catches some, not all. Dotfiles yes, system paths no.
- 📝 Use REQUEST_URI. It works across all HTTP versions including HTTP/3.
- 🔒 Layer your defenses. WAF rules + firewall blocks + monitoring.
- 🚨 Watch for scanners. When you see one IP probing, expect more.
The attackers are automated and relentless. Your defenses should be too.
Stay patched, stay paranoid. 🛡️
Related Resources:
- CRS Issue #4404 – Vite.js coverage discussion
Leave a Reply