Skip to content

fix(security): use CSPRNG for password and OTP generation#3477

Open
MaxwellCalkin wants to merge 1 commit intosimstudioai:mainfrom
MaxwellCalkin:fix/insecure-random-password-otp
Open

fix(security): use CSPRNG for password and OTP generation#3477
MaxwellCalkin wants to merge 1 commit intosimstudioai:mainfrom
MaxwellCalkin:fix/insecure-random-password-otp

Conversation

@MaxwellCalkin
Copy link

Summary

generatePassword() and generateOTP() use Math.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.tsgeneratePassword(): Replace Math.random() with randomBytes() from Node.js crypto (already imported in this file). Used to generate deployment passwords that protect published chat widgets.
  • app/api/chat/[identifier]/otp/route.tsgenerateOTP(): Replace Math.floor(100000 + Math.random() * 900000) with crypto.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:

  • Guessing upcoming OTP codes to bypass email authentication on deployed chats
  • Predicting generated passwords for password-protected deployments

Test plan

  • Existing generatePassword tests in encryption.test.ts continue to pass (length, character set, uniqueness)
  • OTP email flow still generates valid 6-digit codes and verifies correctly
  • No changes to public API surface

This PR was authored by Claude Opus 4.6 (AI), operated by @MaxwellCalkin

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>
@cursor
Copy link

cursor bot commented Mar 9, 2026

PR Summary

Medium Risk
Touches authentication-related credential/OTP generation; while intended as a drop-in entropy-source swap, any subtle behavior/compatibility differences could impact login or deployment password flows.

Overview
Improves security of generated secrets by replacing Math.random() with crypto-grade randomness in two places.

generateOTP() in app/api/chat/[identifier]/otp/route.ts now uses crypto.randomInt(100000, 1000000) for 6-digit email verification codes, and generatePassword() in lib/core/security/encryption.ts now derives characters from crypto.randomBytes() to generate passwords with CSPRNG entropy while keeping the same output format/charset.

Written by Cursor Bugbot for commit a7d0f4b. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 9, 2026 2:16am

Request Review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR replaces insecure Math.random() calls with cryptographically secure alternatives from Node's crypto module in two security-critical code paths: OTP generation for email-based chat authentication and password generation for password-protected deployments.

  • otp/route.ts: Math.random() replaced with crypto.randomInt(100000, 1000000) — this is a clean, correct fix that provides cryptographically secure, uniformly distributed 6-digit codes.
  • encryption.ts: Math.random() replaced with randomBytes(length) for password generation. The character selection uses bytes[i] % chars.length, which introduces a minor modulo bias (non-uniform character distribution of ~1.5%) since 256 is not evenly divisible by the 76-character charset. Using randomInt(0, chars.length) would provide perfectly uniform selection and is more idiomatic.

Both changes preserve the same output format and API surface. The PR achieves its main security objective of eliminating Math.random() for cryptographic purposes. The OTP fix is optimal; the password generator has a minor best-practice improvement opportunity.

Confidence Score: 4/5

  • This PR is safe to merge — it is a net security improvement that eliminates the use of Math.random() for security-sensitive values, with one minor best-practice gap in the password generator.
  • The PR successfully addresses the core vulnerability by replacing Math.random() with CSPRNG in both locations. The OTP fix is optimal. The password generator has a minor modulo bias (~1.5% non-uniformity) due to the bytes[i] % chars.length approach, which could be improved by using randomInt instead, but does not introduce a practical vulnerability. Score of 4 rather than 5 reflects this addressable imperfection that is worth discussing before merge.
  • apps/sim/lib/core/security/encryption.ts — the modulo bias in character selection is worth reviewing for best-practice uniformity.

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"]
Loading

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant