🔍 Vibe & Verify
The AI gave us this. It runs fine. But would it survive a real user? Read the snippet, decide what you'd fix before shipping, then reveal the answer. This is rigor as a game — and the whole pedagogy is that when you play at fixing the flaw, you remember it next time.
The taped-on key 🔑
The AI gave us this. It runs fine. But would it survive a real user?
const stripe = require('stripe')("sk_live_51Hx...realkey...");🔍 What would you fix before shipping?— Hide the fix
The flaw
a live secret key hardcoded in the source. Anyone who sees the repo can charge cards / drain the account.
The fix
move it to an environment variable — `require('stripe')(process.env.STRIPE_SECRET_KEY)` — add `.env` to `.gitignore`, and rotate the exposed key.
The trusting form 🧹
The AI gave us this. It runs fine. But would it survive a real user?
app.post('/transfer', (req, res) => {
const amount = req.body.amount;
account.balance -= amount; // whatever the user sends, we believe
});🔍 What would you fix before shipping?— Hide the fix
The flaw
no validation. A user can send a negative amount (and *gain* money), a string, or nothing.
The fix
validate on the server — check it's a positive number within allowed limits before touching the balance.
The fake button ♿
The AI gave us this. It runs fine. But would it survive a real user?
<div onclick="submitForm()" class="btn">Submit</div>🔍 What would you fix before shipping?— Hide the fix
The flaw
a `<div>` pretending to be a button. Keyboard users can't tab to it or press Enter; screen readers don't announce it as a button.
The fix
use a real element — `<button type="submit">Submit</button>`. Semantic HTML is free accessibility.
The open door 🚪
The AI gave us this. It runs fine. But would it survive a real user?
app.get('/api/orders/:id', (req, res) => {
const order = db.getOrder(req.params.id); // no check who's asking
res.json(order);
});🔍 What would you fix before shipping?— Hide the fix
The flaw
IDOR. Any logged-in user can read **anyone's** order by changing the ID in the URL.
The fix
verify ownership on the backend — `if (order.userId !== req.user.id) return res.status(403)`.
The plaintext password 🔓
The AI gave us this. It runs fine. But would it survive a real user?
db.users.insert({ email, password }); // stored exactly as typed🔍 What would you fix before shipping?— Hide the fix
The flaw
passwords saved in plain text. One database leak exposes every user's actual password (which they reuse elsewhere).
The fix
hash with bcrypt/argon2 before storing — `const hash = await bcrypt.hash(password, 12)` — and store the hash, never the password.
🎯 The String-Stitched Query
The AI gave us this. It runs fine. But would it survive a real user?
app.get('/users', async (req, res) => {
const role = req.query.role;
const result = await db.query(
"SELECT * FROM users WHERE role = '" + role + "'"
);
res.json(result.rows);
});🔍 What would you fix before shipping?— Hide the fix
The flaw
The `role` value comes straight from the URL and is dropped raw into the SQL string. An attacker can pass `' OR '1'='1` (or worse, `'; DROP TABLE users; --`) and the database executes it — classic SQL injection, leaking or destroying data.
The fix
Use a parameterized query: `db.query("SELECT * FROM users WHERE role = $1", [role])`. The driver sends the value as data, never as executable SQL, so the injection is structurally impossible.
🐢 The N+1 Stampede
The AI gave us this. It runs fine. But would it survive a real user?
const orders = await db.query("SELECT * FROM orders");
for (const order of orders.rows) {
const user = await db.query(
"SELECT * FROM users WHERE id = $1",
[order.user_id]
);
order.user = user.rows[0];
}
res.json(orders.rows);🔍 What would you fix before shipping?— Hide the fix
The flaw
For every order returned, a brand-new round-trip is fired to the database. Ten orders = 11 queries; ten thousand orders = 10,001 queries. Response time grows linearly with data volume and the database connection pool collapses under load.
The fix
Replace the loop with a single JOIN — `SELECT orders.*, users.* FROM orders JOIN users ON orders.user_id = users.id` — or collect all `user_id` values and run one `WHERE id = ANY($1::int[])` lookup, then stitch in application code.
⚡ The Racey Balance Update
The AI gave us this. It runs fine. But would it survive a real user?
app.post('/transfer', async (req, res) => {
const { fromId, toId, amount } = req.body;
const sender = await db.query(
'SELECT balance FROM accounts WHERE id = $1', [fromId]
);
if (sender.rows[0].balance < amount) {
return res.status(400).json({ error: 'Insufficient funds' });
}
await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromId]);
await db.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]);
res.json({ ok: true });
});🔍 What would you fix before shipping?— Hide the fix
The flaw
Two concurrent requests can both pass the balance check before either `UPDATE` lands, allowing the same funds to be spent twice (double-spend). The read and the two writes are three separate round-trips with no lock between them.
The fix
Wrap both `UPDATE` statements in a single `BEGIN` / `COMMIT` transaction and collapse the check into the first update: `UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`. If zero rows are affected, roll back and return the error — no race possible.
📦 The Unbounded Firehose
The AI gave us this. It runs fine. But would it survive a real user?
app.get('/products', async (req, res) => {
const products = await db.query('SELECT * FROM products');
res.json(products.rows);
});🔍 What would you fix before shipping?— Hide the fix
The flaw
With 10 rows this is fine; with 2 million rows the server serializes the entire table into RAM, the response payload crushes the client, and the database is tied up for seconds on every call. There is no `LIMIT`, no cursor, no page size.
The fix
Add pagination: read `page` and `limit` from query params (cap `limit` at a sane maximum like 100), then use `LIMIT $1 OFFSET $2` in the query — and never trust the caller to set an unbounded limit.
🔑 The Open Door to Brute Force
The AI gave us this. It runs fine. But would it survive a real user?
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.query(
'SELECT * FROM users WHERE email = $1', [email]
);
const match = await bcrypt.compare(password, user.rows[0].password_hash);
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
res.json({ token: issueJwt(user.rows[0]) });
});🔍 What would you fix before shipping?— Hide the fix
The flaw
There is no throttle, no lockout, no CAPTCHA. An attacker can fire millions of password guesses per minute against any account. Even with bcrypt's cost factor, a fast cluster can still churn through a large wordlist at thousands of attempts per second.
The fix
Apply a rate limiter (e.g., `express-rate-limit` keyed to IP + email) before the handler runs: allow ~5 attempts per 15-minute window, then lock with exponential backoff. Add an account lockout after N consecutive failures and notify the owner.
🕵️ The Secrets-in-Logs Leak
The AI gave us this. It runs fine. But would it survive a real user?
app.post('/login', async (req, res) => {
console.log('Login attempt:', req.body);
const { email, password } = req.body;
// ... authenticate ...
res.json({ token });
});🔍 What would you fix before shipping?— Hide the fix
The flaw
`req.body` on a login route contains the user's plaintext password. That password is now written to every log sink — container stdout, log aggregators, third-party APMs, S3 buckets — where it may persist for months and be readable by anyone with log access, including future breach attackers.
The fix
Never log the raw request body on sensitive routes. Log only safe fields: `console.log('Login attempt for:', req.body.email)`. For debugging, log a redacted copy: `{ ...req.body, password: '[REDACTED]' }`. Apply the same rule to API keys, tokens, and SSNs anywhere in the stack.
🎭 The Forged Webhook
The AI gave us this. It runs fine. But would it survive a real user?
app.post('/webhook/payment', express.json(), async (req, res) => {
const { event, data } = req.body;
if (event === 'payment.succeeded') {
await db.query(
'UPDATE orders SET status = $1 WHERE id = $2',
['paid', data.orderId]
);
}
res.sendStatus(200);
});🔍 What would you fix before shipping?— Hide the fix
The flaw
Any attacker who knows the endpoint URL can POST a fake `payment.succeeded` event and mark unpaid orders as paid. The handler trusts the payload unconditionally — no signature check, no shared secret, nothing preventing forgery.
The fix
Before touching the database, verify the provider's HMAC signature. For example with Stripe: `stripe.webhooks.constructEvent(req.rawBody, req.headers['stripe-signature'], process.env.WEBHOOK_SECRET)`. Use `express.raw()` (not `express.json()`) to preserve the raw bytes needed for HMAC verification.
♻️ The Cache That Forgot to Forget
The AI gave us this. It runs fine. But would it survive a real user?
async function getProductPrice(productId) {
const cached = await redis.get(`price:${productId}`);
if (cached) return JSON.parse(cached);
const row = await db.query(
'SELECT price FROM products WHERE id = $1', [productId]
);
await redis.set(`price:${productId}`, JSON.stringify(row.rows[0].price));
return row.rows[0].price;
}🔍 What would you fix before shipping?— Hide the fix
The flaw
The cache entry is written with no TTL and never invalidated. When a product's price changes in the database, every subsequent read still returns the stale cached value — potentially forever. Users get charged the wrong price; flash-sale discounts never show up.
The fix
Two complementary approaches: set a TTL on write (`redis.set(key, value, 'EX', 300)`) as a safety net, AND explicitly delete or update the cache key inside the same transaction that updates the price — `redis.del(`price:${productId}`)`. Write-through and cache-aside patterns both work; the key is tying invalidation to the write path, not hoping TTL is short enough.
Each Vibe & Verify is a 2-minute lesson disguised as a puzzle. Make rigor fun, and people will choose it.
🏛️ Build It Right, Or Don't Build It At All.