Description
Path traversal lets an attacker use components such as ../ in a file path to access unauthorized files or directories. It commonly occurs when unvalidated input reaches filesystem functions, potentially exposing or overwriting sensitive information or system files.
Potential impact
- Unauthorized access to important system files or other users' data
- Modification or deletion of important files
- Service disruption if essential files are damaged
Remediation
- Check input for path manipulation strings such as
../,/, and\. - Restrict file access to a designated directory and limit absolute paths.
- Normalize paths, for example with Node.js
path.normalize(), and verify that the result stays within the base directory. - Allow only approved filenames or predefined filename rules.
- Distinguish APIs whose first argument is a path, such as
fs.writeFile(path, data), from those taking a file descriptor, such asfs.write(fd, data). Input used only as file content or as data after a descriptor needs a different assessment from path traversal.
Examples
Before
javascript
const fs = require('fs');
const express = require('express');
const app = express();
app.get('/download', (req, res) => {
// Before: use input directly in a filesystem call.
const fileName = req.query.file;
fs.readFile(fileName, (err, data) => {
if (err) return res.status(404).send('File not found');
res.send(data);
});
});
After
javascript
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();
// Only the server may write here, preventing link replacement between checking and reading.
const BASE_DIR = fs.realpathSync(path.join(__dirname, 'files'));
function isInside(baseDir, targetPath) {
const relative = path.relative(baseDir, targetPath);
return (
relative !== '' &&
relative !== '..' &&
!relative.startsWith('..' + path.sep) &&
!path.isAbsolute(relative)
);
}
app.get('/download', (req, res) => {
const rawName = req.query.file;
if (typeof rawName !== 'string') {
return res.status(400).send('Invalid file path');
}
// Allow filenames only; reject directory components instead of stripping them.
const fileName = path.basename(rawName);
if (fileName !== rawName || !/^[a-zA-Z0-9._-]+$/.test(fileName)) {
return res.status(400).send('Invalid file path');
}
const candidate = path.resolve(BASE_DIR, fileName);
if (!isInside(BASE_DIR, candidate)) {
return res.status(400).send('Invalid file path');
}
let requestedPath;
try {
requestedPath = fs.realpathSync(candidate);
} catch {
return res.status(404).send('File not found');
}
if (!isInside(BASE_DIR, requestedPath)) {
return res.status(400).send('Invalid file path');
}
fs.readFile(requestedPath, (err, data) => {
if (err) return res.status(404).send('File not found');
res.send(data);
});
});
app.post('/message', (req, res) => {
// The server fixes the path; input supplies file contents only.
fs.writeFile(path.join(BASE_DIR, 'message.txt'), req.body.message, (err) => {
if (err) return res.status(500).send('write failed');
res.send('saved');
});
});
Explanation:
- Before:
fileNamereachesfs.readFilewithout validation, allowing path components such as../. An attacker may access files the process has permission to read. - After: Check the string type and equality with
path.basename()to reject directory components. Verify that both the normalized path and the real path after symbolic-link resolution remain inside the server-managedBASE_DIR. Input used only as file contents, with a server-fixed path, is a separate concern from path traversal.
The POST excerpt assumes request-body parsing middleware has been configured.