TLDR:
password === secretis wrong.timingSafeEqual+ a per-IP throttle Map are right. Three lines. Do them every time.
The Sweep
I did a full security audit across every an ecommerce business app in mid-June — eleven apps, dozens of findings, two weeks of commits.
Most of the findings were obvious the moment you looked for them.
One wasn't.
The Login That Passed the Vibe Check
An internal lead importer tool is an internal tool — it uploads webinar attendee CSVs, maps them to contacts, and kicks off the lead pipeline.
The login checked the password. Returned a 200 on match, a 401 on fail. Ship it.
But the audit found two holes.
Neither was visible on a quick read. That's exactly what makes them dangerous.
Hole #1: Your Response Time Is Leaking
When you compare strings with ===, JavaScript short-circuits. The moment it finds a mismatched character, it stops.
A character that matches takes a tiny bit longer to process than one that doesn't.
That difference is measured in nanoseconds… but across thousands of probes, it's enough for a patient attacker to recover your password character by character. They send guesses, watch the timing, and narrow it down. No brute force required — just inference.
It's called a timing attack, and === is the mistake that enables it.
The fix is Node's crypto.timingSafeEqual — a comparison that always walks every byte, no matter where the first mismatch is. Response time becomes a flat line. There's one real gotcha: it throws if the two buffers are different lengths, so you hash or normalize both values first to make the lengths equal before you call it.
Hole #2: Nobody Was Watching the Door
There was nothing stopping an attacker from hammering the login endpoint in a loop. No lockout. No throttle. No 429. Just… try as many passwords as you want.
Brute force. No sophistication needed.
The fix: a per-IP throttle wired directly into the route — an in-process Map<ip, {count, resetAt}>. After N failed attempts from the same IP within a window, the endpoint returns 429 and the counter resets automatically. Zero dependencies. No Redis. No session store.
Closes the volume attack entirely.
Two Holes, Two Different Attacks
This is the thing I want you to hold onto.
Timing attack = inference. Slow, surgical, depends on measurement noise. timingSafeEqual blocks it.
Brute force = volume. Fast, dumb, depends on no one stopping it. Rate throttle blocks it.
Neither fix covers the other. You need both.
Why This Matters Beyond This App
This was a P3 — not a critical breach risk, not a data-exposure event. It's an internal tool with a small user base.
I almost moved on.
But the right habit isn't situational. If you get comfortable comparing secrets with ===, you will do it somewhere that matters. Every login, every API key check, every webhook signature validation — they all want constant-time comparison AND a throttle.
Three lines of code. You have no excuse.
P.S. The same
timingSafeEquallogic applies to webhook signatures (Stripe, Twilio, etc.) and API key validation — anywhere you're comparing a user-supplied secret against a stored one. If it's a secret, never use===.