fix(security): use CSPRNG for password and OTP generation#3477
fix(security): use CSPRNG for password and OTP generation#3477MaxwellCalkin wants to merge 1 commit intosimstudioai:mainfrom
Conversation
Replace Math.random() with cryptographically secure alternatives in two security-critical code paths: - generatePassword() in encryption.ts: use randomBytes() (already imported) instead of Math.random() for deployment password generation - generateOTP() in chat OTP route: use crypto.randomInt() instead of Math.random() for authentication code generation Math.random() is not cryptographically secure — its output is predictable and can be reconstructed from a few observed values. Both functions generate authentication material that directly protects user accounts and deployed workflows. This PR was authored by Claude Opus 4.6 (AI), operated by @MaxwellCalkin Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR SummaryMedium Risk Overview
Written by Cursor Bugbot for commit a7d0f4b. This will update automatically on new commits. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| const bytes = randomBytes(length) | ||
| for (let i = 0; i < length; i++) { | ||
| result += chars.charAt(Math.floor(Math.random() * chars.length)) | ||
| result += chars.charAt(bytes[i] % chars.length) |
There was a problem hiding this comment.
Modulo bias in cryptographic password generation
Medium Severity
bytes[i] % chars.length introduces modulo bias because 256 is not evenly divisible by 76 (the charset length). The first 28 characters have a ~33% higher chance of being selected than the remaining 48 characters, reducing the effective entropy of generated passwords. Ironically, this PR aims to improve cryptographic security but introduces a well-known CSPRNG pitfall. crypto.randomInt(0, chars.length) avoids this by using rejection sampling internally.
Greptile SummaryThis PR replaces insecure
Both changes preserve the same output format and API surface. The PR achieves its main security objective of eliminating Confidence Score: 4/5
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["OTP Request (POST /api/chat/:id/otp)"] --> B["generateOTP()"]
B --> B1["crypto.randomInt(100000, 1000000)\n✅ Uniform CSPRNG"]
B1 --> C["storeOTP(email, chatId, otp)"]
C --> D{Storage Method}
D -- Redis --> E["redis.set(key, otp, EX 900)"]
D -- DB --> F["db.insert(verification)"]
E & F --> G["sendEmail(otp)"]
H["Password-Protected Deployment"] --> I["generatePassword(length=24)"]
I --> I1["randomBytes(length)"]
I1 --> I2["bytes[i] % chars.length\n⚠️ Modulo bias (256 % 76 ≠ 0)"]
I2 --> J["Deployment Password"]
K["OTP Verify (PUT /api/chat/:id/otp)"] --> L["getOTP(email, chatId)"]
L --> M{"storedOTP === otp"}
M -- Match --> N["deleteOTP → setChatAuthCookie"]
M -- No match --> O["400 Invalid code"]
Last reviewed commit: a7d0f4b |
| const bytes = randomBytes(length) | ||
| for (let i = 0; i < length; i++) { | ||
| result += chars.charAt(Math.floor(Math.random() * chars.length)) | ||
| result += chars.charAt(bytes[i] % chars.length) |
There was a problem hiding this comment.
Modulo bias reduces character distribution uniformity
bytes[i] % chars.length introduces a modulo bias because 256 is not evenly divisible by chars.length (76). This means the first 256 % 76 = 28 characters in the charset each have a slightly higher probability of selection (4/256) than the remaining characters (3/256), rather than the ideal uniform 1/76.
While this is astronomically more secure than Math.random(), a password generator should strive for uniform distribution. randomInt (already imported in the OTP route) performs rejection sampling internally and provides a perfectly uniform result:
for (let i = 0; i < length; i++) {
result += chars.charAt(randomInt(0, chars.length))
}Note that randomBytes would no longer be needed for this function (though it remains used elsewhere in the file).


Summary
generatePassword()andgenerateOTP()useMath.random()to produce security-critical authentication material.Math.random()is not a cryptographically secure PRNG — its internal state can be reconstructed from a handful of observed outputs, making the generated values predictable.Changes
lib/core/security/encryption.ts—generatePassword(): ReplaceMath.random()withrandomBytes()from Node.jscrypto(already imported in this file). Used to generate deployment passwords that protect published chat widgets.app/api/chat/[identifier]/otp/route.ts—generateOTP(): ReplaceMath.floor(100000 + Math.random() * 900000)withcrypto.randomInt(100000, 1000000). Used to generate 6-digit email verification codes for chat authentication.Both are minimal, drop-in replacements that preserve the same output format and character set. No behavioral changes other than the entropy source.
Why this matters
An attacker who can observe a few outputs from
Math.random()(e.g., by triggering OTP requests) can predict future outputs, enabling:Test plan
generatePasswordtests inencryption.test.tscontinue to pass (length, character set, uniqueness)