• Claude 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

FlawClaude 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 flaggedNot flagged
Decoy: Display name echoed into HTML (looks like XSS, is safe)Not flaggedNot 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 id
app.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) => {Decoy
const 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 view
const 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 search
const 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 download
const 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 update
const 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.

  1. Unzip it: a plain folder and a with-skill folder, each with the app's source, plus the exact prompt.
  2. Add Cloudflare's skill to the with-skill folder with one npx command (in the README).
  3. 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.

Download the kit37 KB