Prevent Hashed Password Leaks

Welcome! In this reading, you will learn how to keep even salted and hashed passwords secure from being accidentally exposed to the client. Just like a restaurant carefully guards their secret sauce recipe, you must ensure your hashed password fields never escape the server. Think of these fields as your most guarded secret—no one, especially outside the server environment, should ever see them.

You will learn:

By the end of this, you’ll see how combining Sequelize scopes with strong hashing practices ensures your users’ passwords remain safe, even if parts of your system are compromised.

Why We Must Guard Hashed Passwords

Hashing (and salting) passwords is a massive step up from storing them in plain text, but it doesn’t mean they’re safe to share freely. If an attacker obtains a user’s hashed password, they can still attempt to crack it using advanced tools or rainbow tables—especially if your hashing algorithm is weak or the passwords are guessable.

Therefore, the best line of defense is: never let the user’s hashed password leave the server in the first place. We only need the hashed password under one circumstance: to verify a password at login. Outside of that, it’s off-limits.

Creating a Scope for Authentication

Let’s imagine we have a User model in Sequelize. We store the hashed password in a field called hashedPassword. We want to retrieve this field only for authentication checks, and we want to hide it from all other operations.

One solution is to create a non-default scope—let’s call it checkPassword—that specifically includes hashedPassword in the attributes. This scope is invoked only during user login. Think of this like a private hallway key: you have it, but you only use it in the rare moments you need to authenticate.

scopes: {
  checkPassword(email) {
    return {
      where: { email },
      attributes: {
        include: ["hashedPassword"] // specifically include hashedPassword
      }
    }
  }
}

Then, when a user tries to log in, you can do:

const user = await User
  .scope({ method: ["checkPassword", email] })
  .findOne();

if (!user) {
  // handle user not found
} else {
  // compare plain text password to user's hashedPassword
  const isMatching = bcrypt.compareSync(plainTextPassword, user.hashedPassword);
  if (isMatching) {
    // password is correct, proceed with login
  } else {
    // invalid password
  }
}

Notice how we specifically invoked the checkPassword scope by passing { method: ["checkPassword", email] } to scope(). This ensures we fetch the user's record and include the hashedPassword in the attributes for that query. Outside of this special scope, the hashed password remains hidden.

Default Scope to Exclude Hashed Password

At this point, we’ve shown how to selectively include the hashed password for authentication. But what about normal queries? If you do a simple User.findAll() or User.findOne() without any scopes, you might accidentally fetch hashedPassword if you forget to exclude it.

Here’s where the default scope helps. By defining a default scope that excludes hashedPassword, you can be confident that any normal query (without a special scope) will never retrieve that field:

defaultScope: {
  attributes: {
    exclude: ["hashedPassword"]
  }
}

Think of this as your big “No Access” sign. Unless you specifically go around the sign (by invoking checkPassword), you won’t see the hashed password. Now, if you run:

const users = await User.findAll();
// none of these user objects will contain hashedPassword

No matter how large your project or how many times you query the User table, you’re safe from accidental leaks of the hashed password.

Step-by-Step Example / Follow-Along

Let’s run through a mock user flow where someone signs in. We’ll assume we have the following User model with these scopes:

class User extends Model {}

User.init({
  email: DataTypes.STRING,
  hashedPassword: DataTypes.STRING,
  // ... other fields ...
}, {
  defaultScope: {
    attributes: {
      exclude: ["hashedPassword"]
    }
  },
  scopes: {
    checkPassword(email) {
      return {
        where: { email },
        attributes: {
          include: ["hashedPassword"]
        }
      }
    }
  },
  sequelize, // pass sequelize instance
  modelName: 'User'
});

Next, we create a login route. We'll keep this super simplified:

// Express login route example
app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  // 1) Retrieve user with hashedPassword included
  const user = await User.scope({ method: ["checkPassword", email] }).findOne();

  // 2) If user doesn't exist, send error
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // 3) Compare hashedPassword with plain text using bcrypt
  const isValid = bcrypt.compareSync(password, user.hashedPassword);
  if (!isValid) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // 4) If match, log user in
  // (In real code, generate a session or token here)
  res.json({ message: 'Logged in successfully' });
});

Notice we are using scope() with checkPassword to load the hashed password only for this moment of authentication. If we used User.findOne() with no scope, the default scope would hide hashedPassword.

Real World Examples

Large social networks or e-commerce sites typically do something similar: they have internal logic to retrieve hashed credentials only during login or password reset flow. Everywhere else—like listing users, showing profiles, or admin dashboards—they never pass hashed passwords back to the client. This drastically reduces the chance of leaking sensitive info.

In addition, consider enterprise apps where administrators can see user details. Even in those scenarios, the hashed password is still hidden. An admin might reset a user’s password, but they should never see the hashed version. That’s top-secret data the server uses only for cryptographic validation.

Relevant Topics to Explore

What You’ve Learned

You’ve seen how to:

Together, these steps form a robust chain of security measures for your application. Think of it as having locked doors and a secret passcode: you only unlock the door (scoped fetch of hashed passwords) when you need to authenticate someone, and you always keep the passcode hidden from everyone else. If you skip any piece—like forgetting to exclude hashed passwords in default scopes—then you leave a door open for attackers.

By properly salting, hashing, and preventing hashed password leaks, you ensure your users’ credentials remain secure at all times. This is a vital responsibility for any modern web developer.