Skip to content

NaN Round Number Parsing #167

@thesmart

Description

@thesmart

Non-numeric characters in the rounds position (e.g., $2b$xx$...) causes parseInt to return NaN, which bypasses security validation and reduced bcrypt to a single round.

Location

index.js lines 1078-1080:

var r1 = parseInt(salt.substring(offset, offset + 1), 10) * 10,
    r2 = parseInt(salt.substring(offset + 1, offset + 2), 10),
    rounds = r1 + r2,

The Bug

When non-numeric characters are in the rounds position, parseInt returns NaN:

  • rounds = NaN + NaN = NaN
  • Line 948: if (rounds < 4 || rounds > 31)PASSES (NaN comparisons are false)
  • Line 964: rounds = (1 << rounds) >>> 01 << NaN = 1 << 0 = 1 round only!

Red tests added to demonstrate issue:

  • invalidRoundsNaNProducesWeakHash: hash contains "$NaN$"
  • invalidRoundsNaN: fully non-numeric rounds "xx"
  • invalidRoundsNaNAsync: async version via callback
  • invalidRoundsPartialNaN: partial non-numeric "1x"

The Fix

The fix validates that rounds contains exactly 2 digits before parsing, rejecting malformed salts with a clear error message.

Severity: LOW

In most real-world scenarios, salts come from genSalt() which always produces valid output. The malformed salt would have to come from:

  • A buggy application
  • Corrupted data
  • Deliberately malicious input to compare()

The real value of this fix is:

  • Defense in depth - reject invalid input rather than silently degrading
  • Fail loudly - better to throw than produce a weak hash
  • Correctness - a crypto library should validate all inputs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions