Skip to content

Commit 9c5f8a2

Browse files
committed
Force hash verification to run every POST /login
Prevent timing attack
1 parent b9b0b1e commit 9c5f8a2

File tree

1 file changed

+19
-11
lines changed

1 file changed

+19
-11
lines changed

nodeJS/authentication/session_based_authentication.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ app.post("/signup", async (req, res, next) => {
368368
});
369369
```
370370
371-
We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash:
371+
We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash.
372372
373373
```javascript
374374
app.post("/login", async (req, res, next) => {
@@ -379,19 +379,15 @@ app.post("/login", async (req, res, next) => {
379379
);
380380
const user = rows[0];
381381

382-
// argon2.verify requires an argon2 hash as the first argument
383-
// so we must early return if there is no matching user
384-
if (!user) {
385-
return res.render("login", {
386-
error: "Incorrect username or password",
387-
});
388-
}
389-
382+
// argon2.verify requires an argon2 hash as its first arg
383+
// so we can't just pass in `undefined` if no user exists.
384+
// The hash itself doesn't matter as long as it's a valid argon2 hash
385+
// since this is to prevent timing attacks if no user is found
390386
const isMatchingPassword = await argon2.verify(
391-
user.password,
387+
user?.password ?? process.env.FALLBACK_HASH,
392388
req.body.password,
393389
);
394-
if (isMatchingPassword) {
390+
if (user && isMatchingPassword) {
395391
req.session.userId = user.id;
396392
res.redirect("/");
397393
} else {
@@ -407,6 +403,18 @@ app.post("/login", async (req, res, next) => {
407403
408404
Now, when a user signs up, their password is salted and hashed before storage, which is then used to verify the password upon login.
409405
406+
<div class="lesson-note lesson-note--warning" markdown="1">
407+
408+
#### Warning: Timing attacks
409+
410+
Why does the `POST /login` middleware force `argon2.verify` to run even when no user is found in our database? Why can't we just early return if no user found?
411+
412+
Just like with [using generic login error messages](#warning-use-generic-login-error-messages), we don't want to reveal that a username is valid and only the corresponding password is incorrect. If no user is found and we return early, then the server will respond quicker than if it had to verify the password against a hash (`argon2.verify` is already designed to account for timing attacks). Attackers could use this timing difference to determine whether a username exists or not, allowing them to focus their efforts on certain usernames - a timing attack.
413+
414+
You don't need to know all the details of specific attack techniques but in this case, it doesn't take much to ensure that the same process always runs regardless of whether a user exists or not.
415+
416+
</div>
417+
410418
### Assignment
411419
412420
<div class="lesson-content__panel" markdown="1">

0 commit comments

Comments
 (0)