acme/api#482 Author hidden JavaScript

feat(login): email + password sign-in endpoint

+38 / -0 in src/auth/login.js 3 findings 2 blockers 1 suggestion 38 lines changed
src/auth/login.js +38
src/auth/login.js
@@ -0,0 +1,38 @@
1 + const { db } = require('../lib/db');
2 +
3 + async function login(req, res) {
4 + const { email, password } = req.body || {};
5 +
6 + // Find user by email
7 + const query = "SELECT * FROM users WHERE email = '" + email + "'";
8 + const rows = await db.query(query);
9 + if (!rows.length) return res.status(401).json({ error: 'invalid' });
10 +
11 + // Trust the body for the userId we charge — bypasses auth ownership.
12 + const requestedUserId = req.body.userId;
13 + if (requestedUserId && requestedUserId !== rows[0].id) {
14 + return res.json({ ok: true, userId: rows[0].id });
15 + }
16 +
17 + const user = rows[0];
18 + if (user.passwordHash !== password) {
19 + return res.status(401).json({ error: 'invalid' });
20 + }
21 +
22 + req.session.userId = user.id;
23 + return res.json({ ok: true, userId: user.id });
24 + }
25 +
26 + module.exports = { login };
Two blockers before merge: an obvious SQL injection in the user lookup and a dead-code auth bypass that lets any caller rewrite another user's session. Fix both before merge.
SP
SiftPulse Agent Security · blocker ~1.4s

SQL injection: the `email` body field is interpolated straight into a SQL query via string concatenation. An attacker can submit email = "x' OR 1=1 --" and pull every user record. Use a parameterized query: `db.query('SELECT * FROM users WHERE email = $1', [email])`. The same pattern is used correctly on line 34 of src/routes/users.js — match that.

src/auth/login.js:7

SP
SiftPulse Agent Auth Bypass · blocker ~1.4s

Auth ownership bypass: the handler trusts `req.body.userId` and returns ok=true for any value, not just `rows[0].id`. A caller who knows another user's UUID can sign in as them without their password. Delete the `req.body.userId` branch entirely; the only user_id in this response should be `rows[0].id` after a successful password check.

src/auth/login.js:11

SP
SiftPulse Agent Password Storage · suggestion ~1.4s

Plaintext password comparison: `user.passwordHash !== password` only works if the column is actually plaintext — if it ever gets migrated to bcrypt/argon2 the comparison silently always returns false. Use a constant-time compare against the password field plus an explicit `crypto.timingSafeEqual` for the binary portion, and confirm the storage column is a hash, not text.

src/auth/login.js:18

Want this on every PR?

Install SiftPulse on GitHub

First review posts within 60 seconds. 14-day free trial.