Skip to main content
interlace
Plugin: node-securityRules

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

AspectDetails
CWE ReferenceCWE-22 (Path Traversal)
SeverityHigh (security vulnerability)
Auto-Fix⚠️ Suggests fixes (manual application)
CategorySecurity
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForNode.js applications, file processing systems, file upload handlers

Value & investment case

Why this rule pays for itself. Framework: cicd-impact/philosophy.md.

DimensionValue
CWECWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)
Feedback-loop tierEditor / 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 relevanceCritical: infra/devtools, B2B SaaS handling user uploads, fintech (document storage) · High: healthtech (medical-record file access), marketplaces · Medium: B2C
Investor-frame impactPath 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 segment

Error 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

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-22 OWASP:A01 CVSS:7.5
Issue DescriptionSpecific vulnerabilityPath Traversal detected
Severity & ComplianceImpact assessmentHIGH [SOC2,PCI-DSS,HIPAA,ISO27001]
Fix InstructionActionable remediationFollow the remediation steps below
Technical TruthOfficial referenceOWASP Top 10

Configuration

OptionTypeDefaultDescription
allowLiteralsbooleanfalseAllow literal string paths
additionalMethodsstring[][]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-object

A 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

AttackExample InputResult
Basic traversal../../../etc/passwdAccess system files
Windows traversal....\\....\\windows\\system32Access Windows files
Encoded traversal%2e%2e%2f%2e%2e%2fetc/passwdURL-encoded attack
Unicode traversal..\\u002f..\\u002fetc/passwdUnicode 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 data

Path 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

Featuredetect-non-literal-fs-filenameeslint-plugin-securityeslint-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

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

⚙️ Options

OptionTypeDefaultDescription
allowLiteralsbooleanfalseAllow literal string paths
additionalMethodsstring[][]Additional fs methods to check
allowedExtensionsstring[][]Allowed file extensions (e.g., [".txt", ".json"])
taintSourcesstring[]Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)
reportUnresolvedPathsbooleanfalseReport 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.