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" | |
| } | |
| } |