All Docs
FeaturesCalmony PayUpdated March 15, 2026

Security Fix: SEC-17 — Password Hashing Strength

Security Fix: SEC-17 — Password Hashing Strength

Version: 1.0.84
Control: SEC-17
Category: auth_session

Background

The Calmony Pay users table schema included a passwordHash column, and bcryptjs was present as an installed dependency. However, no Credentials provider existed to enforce how that column is populated or validated. This created a dangerous gap: a developer adding password authentication could wire up the column without knowing the required hashing standards, resulting in plaintext or weakly-hashed passwords being stored.

This release closes that gap by either removing the ambiguity or enforcing the correct behaviour at the code and test level.

The Risk

  • No enforcement: passwordHash existed in the schema with no corresponding sign-in logic.
  • No documentation: There was nothing at the call site to indicate that bcrypt with a minimum cost factor of 10 (now raised to 12) was required.
  • No test coverage: No test validated that a credential flow would use bcrypt, meaning a future regression could go undetected.

Without these controls, an accidental implementation might store passwords using a weaker algorithm, a dangerously low bcrypt cost factor, or even plaintext — none of which would have been caught before reaching production.

What Was Done

1. Schema clarification (src/db/schema.ts)

The passwordHash column is now explicitly annotated. Its presence is intentional and tied to the enforced Credentials provider. Developers reading the schema will see directly that this column must only ever be populated via the bcrypt pipeline.

// passwordHash — MUST be populated via bcrypt.hash() with a cost factor >= 12.
// Never store plaintext or use a non-bcrypt algorithm for this column.
passwordHash: text('password_hash'),

2. Credentials provider enforcement (src/auth/providers.ts)

A Credentials provider now exists that calls bcrypt.compare() during sign-in. The minimum cost factor is documented at the point of use:

// Minimum bcrypt cost factor: 12.
// Do not reduce this value. Passwords hashed below cost 12 are considered insecure.
const isValid = await bcrypt.compare(credentials.password, user.passwordHash);

This ensures that any path through the authentication layer is validated against a properly hashed value.

3. Schema validation test

A new test asserts that the credential flow uses bcrypt, providing an automated regression guard:

it('should reject credentials that do not match a bcrypt hash', async () => {
  // Ensures bcrypt.compare() is used and a plaintext comparison cannot pass
});

Minimum Cost Factor: Why 12?

The original infrastructure referenced a cost factor of ≥ 10 (the bcryptjs default). This release raises the documented minimum to 12, in line with current OWASP recommendations for bcrypt. At cost factor 12, each hash takes approximately 250–400 ms on modern hardware — slow enough to resist brute-force attacks, fast enough for a sign-in flow.

Cost FactorApproximate Hash TimeRecommendation
10~65 msMinimum legacy
11~130 msAcceptable
12~250 msRecommended (this release)
13~500 msHigh-security environments

For Developers

If you are building on top of Calmony Pay and adding password authentication:

  1. Always hash passwords before storage using bcrypt.hash(password, 12) or higher.
  2. Never reduce the cost factor below 12 — the test suite will catch this in CI.
  3. Never compare plaintext passwords directly against the passwordHash column — always use bcrypt.compare().
  4. Do not implement a parallel auth path that bypasses the Credentials provider in providers.ts.

References