<!-- codemore-ignore-file: core-security-path-traversal -->
core-security-path-traversal
| Category | Default severity | Lifecycle | Default confidence |
|---|---|---|---|
| security | BLOCKER | beta | 0.8 |
What it catches
User-supplied filenames concatenated onto a file-system path without an inside-base guard. Maps to CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and OWASP A03 (Injection).
Detects patterns like:
fs.readFile(req.params.name, ...)— no path validationfs.createReadStream(BASE + req.query.name)— user string appended to baseopen(f'/uploads/{filename}')— f-string with user inputres.sendFile(path.join(UPLOADS, req.params.f))— no inside-base check
Why it matters
An attacker sends a filename like ../../etc/passwd and the application concatenates it directly into open() or readFile(). The attacker can now read any file the process can access. The defence is two lines: resolve the candidate path to an absolute path, then refuse anything that doesn't sit inside your designated directory.
This is a top-10 web vulnerability and a reliable way to leak .env files, private keys, and other users' uploaded data.
Example — flagged
import fs from 'fs';
import { NextRequest } from 'next/server';
export async function GET(req: NextRequest) {
const filename = req.nextUrl.searchParams.get('name');
// BLOCKER: user input concatenated into readFile with no path guard.
const contents = fs.readFileSync(`/uploads/${filename}`, 'utf-8');
return new Response(contents);
}def serve_doc(request):
# BLOCKER: user input from request.params directly into open().
name = request.params['name']
with open(f'/var/docs/{name}', 'r') as f:
return f.read()Example — not flagged
import fs from 'fs';
import path from 'path';
export async function GET(req: NextRequest) {
const filename = req.nextUrl.searchParams.get('name');
// OK: resolve to absolute path, then check it's inside the safe base.
const BASE = path.resolve('./uploads');
const candidate = path.resolve(BASE, filename);
if (!candidate.startsWith(BASE + path.sep)) {
return new Response('Path escapes base', { status: 400 });
}
const contents = fs.readFileSync(candidate, 'utf-8');
return new Response(contents);
}import os
def serve_doc(request):
# OK: secure_filename + startswith check.
from werkzeug.utils import secure_filename
BASE = os.path.abspath('/var/docs')
name = secure_filename(request.params['name'])
candidate = os.path.abspath(os.path.join(BASE, name))
if not candidate.startswith(BASE + os.sep):
raise PermissionError("path escapes base")
with open(candidate, 'r') as f:
return f.read()Suggested fix
Always validate file paths with a base-directory guard:
Node.js:
import path from 'node:path';
const BASE = path.resolve(uploadsDir);
const candidate = path.resolve(BASE, userSuppliedFilename);
if (!candidate.startsWith(BASE + path.sep)) {
return res.status(400).send('invalid path');
}
// Safe to open candidate now.Python:
import os
from werkzeug.utils import secure_filename
BASE = os.path.abspath('/safe/directory')
candidate = os.path.abspath(os.path.join(BASE, filename))
if not candidate.startswith(BASE + os.sep):
raise PermissionError("path escapes base")Suppression
// Reason: filename comes from a trusted internal queue, never user input.
// codemore-ignore-next-line: core-security-path-traversal
const data = fs.readFileSync(`/archive/${queuedFilename}`, 'utf-8');The directive must be on the line immediately before the target. If you put a comment between them, the directive suppresses the comment instead.
References
Implementation
Regex scan for file-open call sites (fs.readFile, open(), send_file, etc.) and checks whether the captured argument contains a user-input hint (req, params, query, filename) AND lacks a nearby guard (path.resolve / os.path.abspath + startswith check within ~7 lines).
Source: `shared/packs/core-security/core-security-path-traversal.ts` Fixtures: `corpus/rules/core-security-path-traversal/`