/** * vulnerable-api.js.txt * ===================== * INTENTIONALLY VULNERABLE Node.js Express API for CTF / code-auditing practice. * DO NOT deploy or run in production. All vulnerabilities are educational. */ const express = require('express'); const jwt = require('jsonwebtoken'); const axios = require('axios'); const mongoose = require('mongoose'); const _ = require('lodash'); const app = express(); app.use(express.json()); // VULNERABILITY: Weak JWT secret and no expiry const JWT_SECRET = 'secret'; // TODO: use strong secret from env const UserSchema = new mongoose.Schema({ username: String, password: String, // stored in plaintext -- TODO: hash passwords role: { type: String, default: 'user' }, email: String }); const User = mongoose.model('User', UserSchema); const DocumentSchema = new mongoose.Schema({ title: String, content: String, owner: String }); const Document = mongoose.model('Document', DocumentSchema); // ---------- Authentication ---------- // VULNERABILITY: Broken authentication -- JWT without expiry, weak secret app.post('/api/login', async (req, res) => { const { username, password } = req.body; const user = await User.findOne({ username, password }); // plaintext compare if (!user) return res.status(401).json({ error: 'Invalid credentials' }); // Vulnerable: no expiresIn, weak secret, algorithm not pinned const token = jwt.sign( { id: user._id, username: user.username, role: user.role }, JWT_SECRET ); // TODO: add expiration res.json({ token }); }); // Middleware -- VULNERABILITY: does not verify algorithm, allows "none" function authMiddleware(req, res, next) { const header = req.headers.authorization; if (!header) return res.status(401).json({ error: 'No token' }); try { // Vulnerable: algorithms not restricted -- attacker can use "none" req.user = jwt.verify(header.replace('Bearer ', ''), JWT_SECRET); next(); } catch (e) { res.status(403).json({ error: 'Invalid token' }); } } // ---------- Routes ---------- // VULNERABILITY: NoSQL Injection (user-controlled query object) app.post('/api/users/search', authMiddleware, async (req, res) => { const { username, role } = req.body; // Vulnerable: if username is { "$gt": "" }, returns all users const users = await User.find({ username, role }); // TODO: validate input types res.json(users); }); // VULNERABILITY: IDOR -- no ownership check on document access app.get('/api/documents/:id', authMiddleware, async (req, res) => { // Vulnerable: any authenticated user can access any document by ID const doc = await Document.findById(req.params.id); // TODO: check ownership if (!doc) return res.status(404).json({ error: 'Not found' }); res.json(doc); }); app.delete('/api/documents/:id', authMiddleware, async (req, res) => { // Vulnerable: no ownership or role check before deletion await Document.findByIdAndDelete(req.params.id); res.json({ message: 'Deleted' }); }); // VULNERABILITY: SSRF -- user-controlled URL fetched by server app.post('/api/fetch-url', authMiddleware, async (req, res) => { const { url } = req.body; try { // Vulnerable: no URL validation -- attacker can request internal services // e.g., http://169.254.169.254/latest/meta-data/ (AWS metadata) // e.g., http://localhost:6379/ (internal Redis) const response = await axios.get(url); // TODO: validate URL domain res.json({ status: response.status, data: response.data }); } catch (e) { res.status(500).json({ error: e.message }); } }); // VULNERABILITY: Prototype Pollution via lodash merge app.put('/api/settings', authMiddleware, async (req, res) => { let settings = { theme: 'dark', notifications: true }; // Vulnerable: deep merge of user-controlled object into settings // Payload: { "__proto__": { "isAdmin": true } } _.merge(settings, req.body); // TODO: sanitize input keys res.json({ message: 'Settings updated', settings }); }); // VULNERABILITY: Mass Assignment -- user can set their own role app.put('/api/profile', authMiddleware, async (req, res) => { // Vulnerable: directly applies all user-supplied fields including 'role' const updated = await User.findByIdAndUpdate(req.user.id, req.body, { new: true }); res.json(updated); }); // VULNERABILITY: Verbose error leaking stack traces app.use((err, req, res, next) => { res.status(500).json({ error: err.message, stack: err.stack, // leaks internal paths and code structure env: process.env.NODE_ENV }); }); app.listen(3000, '0.0.0.0', () => { console.log('API running on port 3000'); });