detect-non-literal-fs-filename
Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system
CWE: CWE-693
OWASP Mobile: OWASP Mobile Top 10
Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system. This rule is part of eslint-plugin-node-security and provides LLM-optimized error messages with fix suggestions.
🚨 Security rule | 💡 Provides LLM-optimized guidance | ⚠️ Set to warn in recommended
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-22 (Path Traversal) |
| Severity | High (security vulnerability) |
| Auto-Fix | ⚠️ Suggests fixes (manual application) |
| Category | Security |
| ESLint MCP | ✅ Optimized for ESLint MCP integration |
| Best For | Node.js applications, file processing systems, file upload handlers |
Value & investment case
Why this rule pays for itself. Framework:
cicd-impact/philosophy.md.
| Dimension | Value |
|---|---|
| CWE | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal) |
| Feedback-loop tier | Editor / pre-commit (sub-second) — cheapest layer per the feedback-loop hierarchy |
| Defensive-layer leverage | ~10× cheaper than unit-test · ~1,000× cheaper than production rollback · 10,000+× cheaper than customer disclosure (cost-ratio anchors) |
| Niche relevance | Critical: infra/devtools, B2B SaaS handling user uploads, fintech (document storage) · High: healthtech (medical-record file access), marketplaces · Medium: B2C |
| Investor-frame impact | Path traversal → unauthorized file access (configs, source code, secrets) or unauthorized writes (overwriting system files). Single bug class that often escalates to full system compromise; catch at lint-time is the cheapest possible defense. |
Read also: philosophy.md §investor-frame · niche-presets.json · analyzer-evaluation-framework.md
Vulnerability and Risk
Vulnerability: Using non-literal (dynamic) values for filesystem operations (like opening, reading, or writing files) without strict validation allows users to control file paths.
Risk: Attackers can manipulate file paths to access files outside the intended directory (Path Traversal), overwriting critical system files, or disclosing sensitive information (like configuration files or source code).
Rule Details
This rule detects dangerous use of Node.js fs methods with dynamic paths that can lead to path traversal attacks (also known as directory traversal).
Traversal needs a path to traverse out of
CWE-22 is about escaping a directory the code chose, by extending or
redirecting a path that has other, fixed parts. path.join('/uploads', userFile) is that shape: ../../etc/passwd walks out of /uploads. A tainted
value used entire is not:
// Not reported — twilio-node src/base/RequestClient.ts:128
agentOpts.ca = fs.readFileSync(process.env.TWILIO_CA_BUNDLE);There is no base directory to escape and nothing to append to. Whoever sets
TWILIO_CA_BUNDLE names a file outright — and anyone who can set a variable in
the process environment already chooses which files the process opens, with or
without this line. Reporting it as traversal describes a mechanism that is not
present.
So a taint source reaching an fs path reports only when it has been composed
into a path: a template with literal text around it, a + concatenation, or
a path.join / path.resolve with more than one argument.
fs.readFileSync(process.env.CA); // silent — whole value
fs.readFileSync(path.resolve(process.env.CA)); // silent — normalised, still whole
fs.readFileSync('/etc/app/' + process.env.NAME); // reported — a prefix to escape
fs.cpSync(path.join(__dirname, 'packages', argv[2])); // reported — a base plus a segmentError Message Format
The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:
🔒 CWE-22 OWASP:A01 CVSS:7.5 | Path Traversal detected | HIGH [SOC2,PCI-DSS,HIPAA,ISO27001]
Fix: Review and apply the recommended fix | https://owasp.org/Top10/A01_2021/Message Components
| Component | Purpose | Example |
|---|---|---|
| Risk Standards | Security benchmarks | CWE-22 OWASP:A01 CVSS:7.5 |
| Issue Description | Specific vulnerability | Path Traversal detected |
| Severity & Compliance | Impact assessment | HIGH [SOC2,PCI-DSS,HIPAA,ISO27001] |
| Fix Instruction | Actionable remediation | Follow the remediation steps below |
| Technical Truth | Official reference | OWASP Top 10 |
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
allowLiterals | boolean | false | Allow literal string paths |
additionalMethods | string[] | [] | Additional fs methods to check |
How the fs module is recognised
The rule resolves the binding rather than matching one spelling of it, so all
of these are checked — fs, node:fs, fs/promises and node:fs/promises:
import fs from 'fs'; // default import, any local name
import * as fileSystem from 'node:fs'; // namespace import
import { readFile } from 'fs/promises'; // named import, renamed or not
const { readdir } = require('node:fs'); // destructured require
fs.promises.readFile(p); // the promises sub-objectA bare fs.readFile(...) in a file that imports nothing is still reported, on
the assumption that fs names the module.
Bindings are resolved across the whole file before any call is judged, so a
require placed below its call site still counts — statement order is not a
security property.
Examples
❌ Incorrect
// Path traversal - HIGH risk
const { readFile } = require('fs');
readFile(userPath, callback); // Attacker can access ../../../etc/passwd
// Directory traversal - HIGH risk
fs.readdir(userDir, callback); // Can list any directory
// File creation - MEDIUM risk
fs.writeFile(userFile, data, callback); // Can write anywhere✅ Correct
fs.readFile("/path/to/file.txt", callback);Path Traversal Prevention
Basic Protection
const path = require('path');
// ❌ Vulnerable
fs.readFile(userInput, callback);
// ✅ Protected
const safePath = path.resolve(SAFE_DIR, userInput);
if (!safePath.startsWith(SAFE_DIR)) {
return callback(new Error('Invalid path'));
}
fs.readFile(safePath, callback);Advanced Protection
function securePath(baseDir: string, userPath: string): string {
const resolved = path.resolve(baseDir, userPath);
// Check if resolved path is within base directory
if (!resolved.startsWith(baseDir)) {
throw new Error('Path traversal detected');
}
// Additional security: remove any remaining ..
const normalized = path.normalize(resolved);
if (normalized.includes('..')) {
throw new Error('Invalid path segments');
}
return resolved;
}Common Attack Vectors
| Attack | Example Input | Result |
|---|---|---|
| Basic traversal | ../../../etc/passwd | Access system files |
| Windows traversal | ....\\....\\windows\\system32 | Access Windows files |
| Encoded traversal | %2e%2e%2f%2e%2e%2fetc/passwd | URL-encoded attack |
| Unicode traversal | ..\\u002f..\\u002fetc/passwd | Unicode bypass |
Method-Specific Guidance
File Reading (readFile, readFileSync)
- Use
path.basename()to strip directory components - Combine with safe base directory
- Validate file extensions if needed
File Writing (writeFile, writeFileSync)
- Same as reading, but consider separate write directories
- Check disk space and file size limits
- Validate file types on upload
Directory Operations (readdir, stat)
- Always resolve and validate directory paths
- Check directory existence and permissions
- Consider rate limiting for directory listings
Stream Operations (createReadStream, createWriteStream)
- Apply same path validation as regular file operations
- Be careful with relative paths in streams
- Validate stream destinations
Security Best Practices
Directory Structure
project/
├── uploads/ # User uploads (read-only from here)
├── public/ # Public files
├── temp/ # Temporary files
└── user-data/ # User-specific dataPath Validation
class SecurePath {
constructor(private baseDir: string) {}
resolve(userPath: string): string {
const resolved = path.resolve(this.baseDir, userPath);
// Security checks
if (!resolved.startsWith(this.baseDir)) {
throw new Error('Path traversal attempt');
}
// Remove dangerous segments
const normalized = path.normalize(resolved);
if (normalized.includes('..') || normalized.includes('\0')) {
throw new Error('Invalid path');
}
return resolved;
}
}Migration Guide
Phase 1: Discovery
{
rules: {
'node-security/detect-non-literal-fs-filename': 'warn'
}
}Phase 2: Implementation
// Add security utilities
const SECURE_PATHS = {
uploads: path.join(__dirname, 'uploads'),
public: path.join(__dirname, 'public'),
temp: path.join(__dirname, 'temp')
};
// Replace unsafe calls
fs.readFile(userPath) → fs.readFile(securePath.resolve(userPath))Phase 3: Testing
// Test path traversal attempts
const attacks = [
'../../../etc/passwd',
'..\\..\\windows\\system32',
'....//....//etc/passwd',
];
for (const attack of attacks) {
expect(() => securePath.resolve(attack)).toThrow();
}Comparison with Alternatives
| Feature | detect-non-literal-fs-filename | eslint-plugin-security | eslint-plugin-node |
|---|---|---|---|
| Path Traversal Detection | ✅ Yes | ⚠️ Limited | ⚠️ Limited |
| CWE Reference | ✅ CWE-22 included | ⚠️ Limited | ⚠️ Limited |
| LLM-Optimized | ✅ Yes | ❌ No | ❌ No |
| ESLint MCP | ✅ Optimized | ❌ No | ❌ No |
| Fix Suggestions | ✅ Detailed | ⚠️ Basic | ⚠️ Basic |
Related Rules
detect-eval-with-expression- Prevents code injection via eval()detect-child-process- Prevents command injectiondetect-object-injection- Prevents prototype pollutiondetect-non-literal-regexp- Prevents ReDoS attacks
Known False Negatives
The following patterns are not detected due to static analysis limitations:
A whole tainted value used as the entire path
fs.readFileSync(process.env.X) is silent by design — see Traversal needs a
path to traverse out of above. If your threat model treats the environment or
process.argv as attacker-controlled and you want every fs call driven by
them flagged regardless of shape, that is no-arbitrary-file-access's
question, not this rule's.
Path from Variable
Why: Path strings from variables not traced.
// ❌ NOT DETECTED - Path from variable
const filePath = userInput;
fs.readFile(filePath);Mitigation: Validate and sanitize all paths.
Indirect Path Construction
Why: Complex path building not analyzed.
// ❌ NOT DETECTED - Indirect
const path = buildPath(base, userInput);
fs.readFile(path);Mitigation: Use path whitelisting.
Custom FS Wrappers
Why: FS wrappers not recognized.
// ❌ NOT DETECTED - Wrapper
fileManager.read(userPath);Mitigation: Apply rule to wrapper implementations.
Further Reading
- OWASP Path Traversal - Path traversal attack guide
- Node.js File System Security - Node.js fs module security
- CWE-22: Path Traversal - Official CWE entry
- ESLint MCP Setup - Enable AI assistant integration
⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
allowLiterals | boolean | false | Allow literal string paths |
additionalMethods | string[] | [] | Additional fs methods to check |
allowedExtensions | string[] | [] | Allowed file extensions (e.g., [".txt", ".json"]) |
taintSources | string[] | — | Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process) |
reportUnresolvedPaths | boolean | false | Report paths whose provenance cannot be resolved. Restores the pre-inversion behaviour; measured at 7% precision on real code. |
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.