Safeguard
Open Source

bcrypt on npm: A Security Review and Safe Usage Guide

The bcrypt npm package is a solid password-hashing choice, but its 72-byte input limit and native build story create footguns worth understanding before you ship.

Karan Patel
Platform Engineer
6 min read

The bcrypt npm package is a well-maintained, widely used binding to the bcrypt password-hashing algorithm, and for most Node.js applications it's a sound choice — provided you understand its 72-byte input limit and use it with the right cost factor. bcrypt is an adaptive hash designed specifically for passwords: it's deliberately slow, it salts automatically, and its work factor can be raised as hardware gets faster. Those are exactly the properties you want. The footguns are subtle and worth walking through before you build auth on top of it.

What bcrypt is and why it's a good default

Plain hashes like SHA-256 are the wrong tool for passwords because they're fast, which is a gift to anyone running an offline cracking rig. bcrypt is intentionally expensive and includes a per-password salt, so precomputed rainbow tables are useless and brute force is slow. The bcrypt npm package wraps the C implementation for performance.

Basic correct usage:

const bcrypt = require('bcrypt')

const SALT_ROUNDS = 12

// hashing on signup
const hash = await bcrypt.hash(plaintextPassword, SALT_ROUNDS)

// verifying on login
const ok = await bcrypt.compare(submittedPassword, hash)

The cost factor (SALT_ROUNDS) is the dial that matters. Each increment doubles the work. A value around 12 is a reasonable modern baseline; benchmark on your production hardware and pick the highest value that keeps login latency acceptable (roughly 100–300ms per hash is a common target).

The 72-byte limit — the biggest footgun

The most important thing to know about bcrypt is that it only considers the first 72 bytes of the input. Anything beyond that is silently ignored during both hashing and verification. Note that's 72 bytes, not 72 characters — multibyte characters like emoji or many non-Latin scripts consume more than one byte each, so the effective character limit can be well under 72.

The practical consequence: two passwords that share the same first 72 bytes but differ afterward produce the same hash and both validate. For an app where a user pastes a very long passphrase or a password manager generates one, this is a real correctness and security issue.

Two safe ways to handle it:

  1. Validate length. Reject or cap passwords over 72 bytes so the truncation never surprises anyone.
  2. Pre-hash. Run the password through SHA-256, then base64-encode the digest, then feed that to bcrypt. This is the technique Django and others use to safely support arbitrarily long passwords. Be careful to encode the digest (base64/hex) rather than passing raw bytes, so null bytes don't cause their own truncation issue.
const crypto = require('crypto')

function prehash(password) {
  return crypto.createHash('sha256').update(password).digest('base64')
}

const hash = await bcrypt.hash(prehash(password), 12)

Don't mix approaches across your codebase — pick one and apply it everywhere, or existing hashes won't verify.

Native build vs pure JavaScript

The bcrypt package is a native addon: it compiles C++ during install. That gives good performance but means it needs build tooling on the machine and produces platform-specific binaries, which occasionally breaks in Docker images or serverless environments that differ from your dev box.

The common alternative is bcryptjs, a pure-JavaScript implementation with no native dependency. It installs anywhere and is compatible with hashes produced by bcrypt, at the cost of being slower. If you've fought native build failures in CI or Lambda, bcryptjs is a pragmatic swap. Both share the same 72-byte behavior, so the guidance above applies either way.

Known vulnerabilities and version hygiene

bcrypt has a strong track record, but it isn't CVE-free. Historically, CVE-2020-7689 in node.bcrypt.js involved incorrect truncation when the input length exceeded 255 bytes in versions before the fix, which is one more reason to validate input length and stay current. The package's latest major release is the one to target; it hasn't needed frequent releases lately, which for a stable crypto binding is a sign of maturity rather than neglect, but you should still confirm the version you pin.

Keep it current the boring way:

npm ls bcrypt
npm install bcrypt@latest
npm audit

Because bcrypt sits in your authentication path and pulls in native build dependencies, it's exactly the kind of package worth watching with software composition analysis; an SCA tool such as Safeguard will surface a new advisory against your pinned version and flag vulnerable transitive dependencies you didn't choose directly.

When to reach for argon2 instead

bcrypt is fine, but the current recommendation from password-hashing guidance (including OWASP) leans toward Argon2id for new systems where you can run it. Argon2 is memory-hard, which resists GPU and ASIC cracking better than bcrypt, and it has no 72-byte quirk. In Node, the argon2 package provides it. If you're starting fresh and your platform supports the native module, Argon2id is a strong default; if you already run bcrypt at a healthy cost factor, migrating isn't urgent — you can rehash on next login.

Safe-usage checklist

  • Use a cost factor around 12+, benchmarked on production hardware.
  • Handle the 72-byte limit with length validation or consistent pre-hashing.
  • Always use bcrypt.compare for verification, never a plain string equality check.
  • Pin the version, run npm audit, and watch advisories.
  • Consider bcryptjs if native builds are painful, or Argon2id for new systems.

FAQ

Is the bcrypt npm package safe to use?

Yes, bcrypt is a sound, widely used password-hashing choice for Node.js. The caveats are operational: respect the 72-byte input limit, use an adequate cost factor, keep the version current, and be aware it's a native module that occasionally complicates builds.

Why does bcrypt only use the first 72 bytes?

That's a property of the underlying bcrypt algorithm, not a bug in the npm package. Input beyond 72 bytes is ignored during hashing and verification. Handle it by validating password length or pre-hashing with SHA-256 and base64-encoding before passing to bcrypt.

Should I use bcrypt or bcryptjs?

Use bcrypt (native) for better performance when your build environment supports compiling native addons. Use bcryptjs (pure JavaScript) when you hit native build problems in Docker, CI, or serverless. Their hashes are interoperable, and both share the 72-byte behavior.

Is bcrypt still recommended, or should I use Argon2?

bcrypt remains acceptable, but current guidance favors Argon2id for new systems because it's memory-hard and avoids the 72-byte limit. If you already run bcrypt at a healthy cost factor you don't need to rush a migration; you can rehash passwords on next successful login.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.