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:
passwordHashexisted 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 Factor | Approximate Hash Time | Recommendation |
|---|---|---|
| 10 | ~65 ms | Minimum legacy |
| 11 | ~130 ms | Acceptable |
| 12 | ~250 ms | Recommended (this release) |
| 13 | ~500 ms | High-security environments |
For Developers
If you are building on top of Calmony Pay and adding password authentication:
- Always hash passwords before storage using
bcrypt.hash(password, 12)or higher. - Never reduce the cost factor below 12 — the test suite will catch this in CI.
- Never compare plaintext passwords directly against the
passwordHashcolumn — always usebcrypt.compare(). - Do not implement a parallel auth path that bypasses the Credentials provider in
providers.ts.
References
- OWASP Password Storage Cheat Sheet
- bcryptjs npm package
- Internal control: SEC-17