Security Challenge
Security Audit Skillc1c8a8c · MITClaude alone
6/6
planted flaws found
- 0/2 decoys flagged
- +7 other real issues
- 3.7 min
Claude + Security Audit Skill
6/6
planted flaws found
- 0/2 decoys flagged
- +6 other real issues
- 35 min, 20 helper agents
A tie on finding the flaws. The skill took about 9× longer and had every finding double-checked by a separate agent.
Each side ran twice; the repeat runs also found 6/6 and flagged no decoys.
The planted flaws
| Flaw | Claude alone | + Skill |
|---|---|---|
| SQL injection in note searchserver.js:170-182 | ||
| Stored XSS in note viewviews.js:97-112 | ||
| Broken access control / IDOR on note viewserver.js:124-129 | ||
| Path traversal in attachment downloadserver.js:200-207 | ||
| Weak password storage (unsalted SHA-256)db.js:10-12 | ||
| Mass assignment / privilege escalation on profile updateserver.js:215-223 | ||
| Decoy: Dynamically built notes filter (looks like SQLi, is safe) | Not flagged | Not flagged |
| Decoy: Display name echoed into HTML (looks like XSS, is safe) | Not flagged | Not flagged |
The code
// Team Notes - a small notes sharing app.const express = require("express");const crypto = require("node:crypto");const fs = require("node:fs");const path = require("node:path");const db = require("./db").init();const { hashPassword, UPLOAD_DIR } = require("./db");const views = require("./views");const app = express();const sessions = new Map(); // token -> user idapp.use(express.urlencoded({ extended: false }));// Parse cookies into req.cookies.app.use((req, res, next) => {req.cookies = {};const header = req.headers.cookie;if (header) {for (const part of header.split(";")) {const i = part.indexOf("=");if (i > -1) {req.cookies[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());}}}next();});// Load the current user from the session cookie.app.use((req, res, next) => {const sid = req.cookies.sid;const uid = sid ? sessions.get(sid) : null;req.user = uid ? db.prepare("SELECT * FROM users WHERE id = ?").get(uid) : null;next();});function requireLogin(req, res, next) {if (!req.user) return res.redirect("/login");next();}function requireAdmin(req, res, next) {if (!req.user || req.user.role !== "admin") {return res.status(403).send("Forbidden");}next();}function startSession(res, userId) {const token = crypto.randomBytes(24).toString("hex");sessions.set(token, userId);res.setHeader("Set-Cookie", `sid=${token}; HttpOnly; Path=/; SameSite=Lax`);}app.get("/", (req, res) => {res.redirect(req.user ? "/notes" : "/login");});// --- Auth ---app.get("/signup", (req, res) => res.send(views.signupPage()));app.post("/signup", (req, res) => {const { username, display_name, password } = req.body;if (!username || !password) {return res.send(views.signupPage("Username and password are required."));}try {const r = db.prepare("INSERT INTO users (username, display_name, password_hash, role) VALUES (?, ?, ?, 'user')").run(username, display_name || username, hashPassword(password));startSession(res, Number(r.lastInsertRowid));res.redirect("/notes");} catch (e) {res.send(views.signupPage("That username is taken."));}});app.get("/login", (req, res) => res.send(views.loginPage()));app.post("/login", (req, res) => {const { username, password } = req.body;const user = db.prepare("SELECT * FROM users WHERE username = ?").get(username);if (!user || user.password_hash !== hashPassword(password)) {return res.send(views.loginPage("Invalid username or password."));}startSession(res, user.id);res.redirect("/notes");});app.post("/logout", (req, res) => {sessions.delete(req.cookies.sid);res.setHeader("Set-Cookie", "sid=; Path=/; Max-Age=0");res.redirect("/login");});// --- Notes ---app.get("/notes", requireLogin, (req, res) => {Decoyconst filter = req.query.filter;let sql ="SELECT n.*, u.username AS owner FROM notes n JOIN users u ON u.id = n.user_id " +"WHERE (n.user_id = ? OR n.id IN (SELECT note_id FROM shares WHERE user_id = ?))";const params = [req.user.id, req.user.id];if (filter) {sql += " AND n.title LIKE ?";params.push("%" + filter + "%");}sql += " ORDER BY n.id DESC";const notes = db.prepare(sql).all(...params);res.send(views.dashboard(req.user, notes, filter));});app.post("/notes", requireLogin, (req, res) => {const title = req.body.title || "Untitled";const body = req.body.body || "";const r = db.prepare("INSERT INTO notes (user_id, title, body, created_at) VALUES (?, ?, ?, ?)").run(req.user.id, title, body, new Date().toISOString());res.redirect("/notes/" + Number(r.lastInsertRowid));});app.get("/notes/:id", requireLogin, (req, res) => {Broken access control / IDOR on note viewconst note = db.prepare("SELECT * FROM notes WHERE id = ?").get(Number(req.params.id));if (!note) return res.status(404).send("Not found");const author = db.prepare("SELECT username FROM users WHERE id = ?").get(note.user_id);res.send(views.notePage(req.user, note, author ? author.username : "unknown"));});app.get("/notes/:id/edit", requireLogin, (req, res) => {const note = db.prepare("SELECT * FROM notes WHERE id = ?").get(Number(req.params.id));if (!note) return res.status(404).send("Not found");if (note.user_id !== req.user.id) return res.status(403).send("Forbidden");res.send(views.editPage(req.user, note));});app.post("/notes/:id/edit", requireLogin, (req, res) => {const note = db.prepare("SELECT * FROM notes WHERE id = ?").get(Number(req.params.id));if (!note) return res.status(404).send("Not found");if (note.user_id !== req.user.id) return res.status(403).send("Forbidden");db.prepare("UPDATE notes SET title = ?, body = ? WHERE id = ?").run(req.body.title || "Untitled",req.body.body || "",note.id);res.redirect("/notes/" + note.id);});app.post("/notes/:id/delete", requireLogin, (req, res) => {const note = db.prepare("SELECT * FROM notes WHERE id = ?").get(Number(req.params.id));if (!note) return res.status(404).send("Not found");if (note.user_id !== req.user.id) return res.status(403).send("Forbidden");db.prepare("DELETE FROM notes WHERE id = ?").run(note.id);res.redirect("/notes");});app.post("/notes/:id/share", requireLogin, (req, res) => {const note = db.prepare("SELECT * FROM notes WHERE id = ?").get(Number(req.params.id));if (!note) return res.status(404).send("Not found");if (note.user_id !== req.user.id) return res.status(403).send("Forbidden");const target = db.prepare("SELECT id FROM users WHERE username = ?").get(req.body.username);if (target) {db.prepare("INSERT INTO shares (note_id, user_id) VALUES (?, ?)").run(note.id, target.id);}res.redirect("/notes/" + note.id);});// --- Search ---app.get("/search", requireLogin, (req, res) => {SQL injection in note searchconst q = req.query.q;let results = [];if (q != null) {const sql ="SELECT id, title FROM notes WHERE user_id = " +req.user.id +" AND title LIKE '%" +q +"%' ORDER BY id DESC";results = db.prepare(sql).all();}res.send(views.searchPage(req.user, q, results));});// --- Attachments ---app.post("/upload", requireLogin, express.raw({ type: "*/*", limit: "5mb" }), (req, res) => {const name = path.basename(req.query.name || "upload.bin");fs.writeFileSync(path.join(UPLOAD_DIR, name), req.body || Buffer.alloc(0));db.prepare("INSERT INTO attachments (user_id, filename) VALUES (?, ?)").run(req.user.id, name);res.redirect("/files");});app.get("/files", requireLogin, (req, res) => {const files = db.prepare("SELECT * FROM attachments WHERE user_id = ? ORDER BY id DESC").all(req.user.id);res.send(views.filesPage(req.user, files));});app.get("/download", requireLogin, (req, res) => {Path traversal in attachment downloadconst file = req.query.file || "";const full = path.join(UPLOAD_DIR, file);fs.readFile(full, (err, data) => {if (err) return res.status(404).send("Not found");res.setHeader("Content-Type", "application/octet-stream");res.send(data);});});// --- Profile ---app.get("/profile", requireLogin, (req, res) => {res.send(views.profilePage(req.user, req.query.saved));});app.post("/profile", requireLogin, (req, res) => {Mass assignment / privilege escalation on profile updateconst fields = req.body;db.prepare("UPDATE users SET display_name = ?, role = ? WHERE id = ?").run(fields.display_name || req.user.display_name,fields.role || req.user.role,req.user.id);res.redirect("/profile?saved=1");});// --- Admin ---app.get("/admin/users", requireLogin, requireAdmin, (req, res) => {const users = db.prepare("SELECT id, username, display_name, role FROM users ORDER BY id").all();res.send(views.adminPage(users));});if (require.main === module) {const port = Number(process.env.PORT) || 3000;const host = process.env.HOST || "127.0.0.1";app.listen(port, host, () => {console.log(`Team Notes running at http://${host}:${port}`);});}module.exports = app;
Run it yourself
A coding agent can't run in a web page, so this is the same challenge as a download, to run in your own Claude Code with your own Claude plan or API key. The app is deliberately vulnerable: review its source, don't deploy it.
- Unzip it: a plain folder and a with-skill folder, each with the app's source, plus the exact prompt.
- Add Cloudflare's skill to the with-skill folder with one npx command (in the README).
- Run the prompt in a fresh Claude Code session in each folder, then compare the findings with the answer key.
Our runs: both sides found 6/6 and flagged no decoys, in 3.7 min alone and 35 min with the skill.