Install with npm i
and run the server with npm start
.
Depending on where you put your project, you might have to alter the path in the query params, but eventually you'll get the /etc/passwd
file downloaded using the unsafe access:
const express = require("express"); | |
const path = require("path"); | |
const fs = require("fs").promises; | |
const app = express(); | |
const PORT = 3000; | |
// Create some dummy files for demonstration | |
async function setupDummyFiles() { | |
// Create user_files directory if it doesn't exist | |
await fs.mkdir("user_files", { recursive: true }); | |
// Create a dummy file in user_files | |
await fs.writeFile( | |
"user_files/safe_file.txt", | |
"This is a safe file you should be able to access", | |
); | |
// Create a dummy .env file for demonstration | |
await fs.writeFile( | |
".env", | |
"SECRET_KEY=this_should_not_be_accessible\nDB_PASSWORD=super_secret", | |
); | |
console.log("✅ Demo files created successfully"); | |
} | |
// ⚠️ Vulnerable endpoint | |
app.get("/download/unsafe", (req, res) => { | |
const userFile = req.query.filename; | |
const filePath = path.join("user_files", userFile); | |
res.sendFile(path.join(process.cwd(), filePath), (err) => { | |
if (err) { | |
res.status(404).send("File not found or not accessible"); | |
} | |
}); | |
}); | |
// ✅ Protected endpoint | |
app.get("/download/safe", (req, res) => { | |
const userFile = req.query.filename; | |
// Sanitize the filename | |
const sanitized = path | |
.basename(userFile) | |
.replace(/[^a-zA-Z0-9.-_]/g, "_") | |
.substring(0, 255); | |
// Whitelist approach - only allow access to specific directory | |
const safePath = path.join(process.cwd(), "user_files", sanitized); | |
if (!safePath.startsWith(path.join(process.cwd(), "user_files"))) { | |
return res.status(403).send("Access denied"); | |
} | |
res.sendFile(safePath, (err) => { | |
if (err) { | |
res.status(404).send("File not found or not accessible"); | |
} | |
}); | |
}); | |
// Add a root route to show instructions | |
app.get("/", (req, res) => { | |
res.send(` | |
<h1>Path Traversal Demo</h1> | |
<h2>Try these attacks:</h2> | |
<ul> | |
<li><a href="/download/unsafe?filename=safe_file.txt">Safe access</a></li> | |
<li><a href="/download/unsafe?filename=../.env">Access .env file (attack)</a></li> | |
<li><a href="/download/unsafe?filename=../../etc/passwd">Access passwd file (attack)</a></li> | |
</ul> | |
<h2>Try the protected endpoint:</h2> | |
<ul> | |
<li><a href="/download/safe?filename=safe_file.txt">Safe access</a></li> | |
<li><a href="/download/safe?filename=../.env">Try to access .env (should fail)</a></li> | |
</ul> | |
`); | |
}); | |
// Start the server | |
async function startServer() { | |
try { | |
await setupDummyFiles(); | |
app.listen(PORT, () => { | |
console.log(` | |
🚀 Server is running! | |
Try these curl commands: | |
curl "http://localhost:${PORT}/download/unsafe?filename=../.env" | |
curl "http://localhost:${PORT}/download/unsafe?filename=../../etc/passwd" | |
Or open in browser: | |
http://localhost:${PORT} | |
Press Ctrl+C to stop the server | |
`); | |
}); | |
} catch (err) { | |
console.error("Failed to start server:", err); | |
} | |
} | |
startServer(); |
{ | |
"name": "attack", | |
"version": "1.0.0", | |
"description": "", | |
"main": "index.js", | |
"scripts": { | |
"start": "node index.js", | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "ISC", | |
"dependencies": { | |
"express": "^4.21.1" | |
} | |
} |