adamp 39b2ce73da Fix critical security vulnerabilities and data integrity issues
- Use timing-safe comparisons for HMAC verification and password checks
- Add login rate limiting (5 attempts/minute per IP)
- Lock down CORS to Vite dev origin only (not needed in production)
- Derive signing key from APP_PASSWORD instead of using it directly
- Replace hand-rolled cookie parsing with cookie-parser middleware
- Wrap all order mutations in SQLite transactions
- Fix TOCTOU race on stock with atomic UPDATE...WHERE quantity >= ?
- Fix APP_SECERT typo in .env (gitignored, local fix only)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 18:04:24 -06:00

97 lines
2.3 KiB
JavaScript

const crypto = require('crypto');
const COOKIE_NAME = 'session';
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
function getSecret() {
if (process.env.APP_SECRET) return process.env.APP_SECRET;
if (process.env.APP_PASSWORD) {
return crypto.createHmac('sha256', 'cookie-tracker-session')
.update(process.env.APP_PASSWORD)
.digest('hex');
}
return null;
}
function sign(payload) {
const secret = getSecret();
if (!secret) return null;
const data = JSON.stringify(payload);
const hmac = crypto.createHmac('sha256', secret).update(data).digest('hex');
const encoded = Buffer.from(data, 'utf8').toString('base64url');
return `${encoded}.${hmac}`;
}
function verify(token) {
const secret = getSecret();
if (!secret || !token) return false;
const parts = token.split('.');
if (parts.length !== 2) return false;
const [encoded, hmac] = parts;
let data;
try {
data = Buffer.from(encoded, 'base64url').toString('utf8');
} catch {
return false;
}
const expected = crypto.createHmac('sha256', secret).update(data).digest('hex');
const expectedBuf = Buffer.from(expected);
const hmacBuf = Buffer.from(hmac);
if (expectedBuf.length !== hmacBuf.length || !crypto.timingSafeEqual(expectedBuf, hmacBuf)) {
return false;
}
let payload;
try {
payload = JSON.parse(data);
} catch {
return false;
}
if (payload.t && Date.now() - payload.t > MAX_AGE_MS) return false;
return true;
}
function createSessionCookie() {
const token = sign({ t: Date.now() });
if (!token) return null;
const opts = {
httpOnly: true,
sameSite: 'lax',
path: '/',
maxAge: Math.floor(MAX_AGE_MS / 1000),
};
if (process.env.NODE_ENV === 'production') opts.secure = true;
return { token, opts };
}
function clearSessionCookie() {
return {
httpOnly: true,
sameSite: 'lax',
path: '/',
maxAge: 0,
};
}
function authMiddleware(req, res, next) {
if (!process.env.APP_PASSWORD) {
return next();
}
const p = req.path || '';
if (p.startsWith('/auth')) return next();
const token = req.cookies?.[COOKIE_NAME] || null;
if (!token || !verify(token)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
module.exports = {
COOKIE_NAME,
getSecret,
sign,
verify,
createSessionCookie,
clearSessionCookie,
authMiddleware,
};