Skip to content

fix(security): harden OTP generation, sanitize(), and verifyApiKey project select#106

Open
prathamtagad wants to merge 2 commits intogeturbackend:mainfrom
prathamtagad:fix/security-otp-sanitize-siteurl
Open

fix(security): harden OTP generation, sanitize(), and verifyApiKey project select#106
prathamtagad wants to merge 2 commits intogeturbackend:mainfrom
prathamtagad:fix/security-otp-sanitize-siteurl

Conversation

@prathamtagad
Copy link
Copy Markdown

@prathamtagad prathamtagad commented Apr 14, 2026

Summary

Fixes three security and functional issues found in the public-api.

1. OTP generated with Math.random() — Cryptographically Weak

Files: apps/public-api/src/controllers/userAuth.controller.js
Lines: 970, 995, 1306, 1344

Math.random() is not a CSPRNG and its output is predictable. Replaced
all four OTP generation sites with crypto.randomInt(100000, 1000000),
which uses Node's secure random source. This matches the pattern already
in use in apps/dashboard-api/src/controllers/auth.controller.js.

2. sanitize() only strips top-level $-prefixed keys — NoSQL Injection Risk

File: packages/common/src/utils/input.validation.js

The previous implementation only removed $-operator keys at the top
level. A payload like { "profile": { "$gt": "" } } would pass through
unchanged, enabling NoSQL injection via nested update/query paths.

The function is now recursive: it walks plain objects and arrays,
stripping $-prefixed keys at every depth.

3. Missing siteUrl in verifyApiKey select — Social Auth Callback Breaks

File: apps/public-api/src/middlewares/verifyApiKey.js

siteUrl and authProviders were not included in the Mongoose
.select() projection. As a result, the project object cached and
forwarded to downstream controllers was missing these fields.
getFrontendCallbackBaseUrl() would always fall back to localhost:5173,
breaking social auth redirects in production.

Both fields are now included in the select clause.

Testing

  • All 90 existing public-api tests pass (npm test -- --runInBand).

Summary by CodeRabbit

  • Bug Fixes
    • Upgraded One-Time Password generation to use cryptographically secure random number generation
    • Enhanced input validation system to properly handle arrays and nested data structures
    • Extended API authentication configuration with additional fields for improved project integration

…lect

- Replace Math.random() with crypto.randomInt() for OTP generation in
  userAuth.controller.js (lines 970, 995, 1306, 1344). Math.random() is
  not cryptographically secure; crypto.randomInt uses Node's CSPRNG,
  consistent with how dashboard-api generates OTPs in auth.controller.js.

- Make sanitize() in input.validation.js recursive so nested himBHsprefixed
  keys (e.g. { profile: { "$gt": "" } }) are stripped at every depth,
  closing a NoSQL injection vector through nested update payloads.

- Add siteUrl and authProviders to the Project.findOne().select() clause
  in verifyApiKey.js. Without these fields the cached project object is
  missing data required by getFrontendCallbackBaseUrl(), causing social
  auth callbacks to always redirect to localhost:5173 in production.

All 90 public-api tests pass.
Copilot AI review requested due to automatic review settings April 14, 2026 06:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Warning

Rate limit exceeded

@prathamtagad has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 37 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b77faf5-47a0-4b9e-b001-ce4245eeeae3

📥 Commits

Reviewing files that changed from the base of the PR and between a61dd3e and 50b1882.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • apps/dashboard-api/src/app.js
  • apps/dashboard-api/src/controllers/project.controller.js
  • apps/dashboard-api/src/middlewares/authMiddleware.js
  • apps/dashboard-api/src/middlewares/loadProjectForAdmin.js
  • apps/public-api/src/__tests__/storage.controller.test.js
  • apps/public-api/src/__tests__/userAuth.refresh.test.js
  • apps/public-api/src/app.js
  • apps/public-api/src/controllers/data.controller.js
  • apps/public-api/src/controllers/storage.controller.js
  • apps/public-api/src/controllers/userAuth.controller.js
  • apps/public-api/src/middlewares/api_usage.js
  • apps/public-api/src/middlewares/blockUsersCollectionDataAccess.js
  • apps/public-api/src/utils/mailLimit.js
  • package.json
  • packages/common/src/config/redis.js
  • packages/common/src/utils/GC.js
  • packages/common/src/utils/encryption.js
📝 Walkthrough

Walkthrough

Three changes enhance security and validation: OTP generation switched to cryptographically secure RNG in user authentication controllers, API key middleware now retrieves additional project fields (siteUrl, authProviders), and input validation function extended to recursively sanitize arrays and nested object properties.

Changes

Cohort / File(s) Summary
OTP Generation Security
apps/public-api/src/controllers/userAuth.controller.js
Replaced Math.random()-based OTP generation with cryptographically secure crypto.randomInt(100000, 1000000).toString() across four authentication paths: signup resend, signup initial registration, resendVerificationOtp, and requestPasswordReset. Redis TTL behavior and email queueing preserved.
API Key Middleware Enhancement
apps/public-api/src/middlewares/verifyApiKey.js
Extended project query projection to include siteUrl and authProviders fields when retrieving project by hashed API key, making these fields available in req.project for downstream handlers.
Input Validation Enhancement
packages/common/src/utils/input.validation.js
Updated sanitize() function to recursively handle arrays and nested object properties. Arrays are mapped and recursively sanitized; object properties are sanitized recursively while excluding keys starting with $. Non-object/non-array values pass through unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #105 — OTP generation security improvement directly addresses replacing Math.random() with cryptographically secure RNG in authentication controller paths.

Poem

🐰 A hop, skip, and crypto leap,
Our secrets now sleep cryptographically deep,
Arrays unwind in recursive embrace,
Validation flows with newfound grace—
Security tightens, our code stands tall! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the three main security fixes: hardened OTP generation, improved sanitize() function, and verifyApiKey project select enhancement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Hardens parts of the public API by improving OTP generation security, strengthening request sanitization against NoSQL operator injection, and expanding the Project projection used during API key verification.

Changes:

  • Replaced OTP generation sites using Math.random() with crypto.randomInt() in userAuth.controller.js.
  • Updated sanitize() to recursively strip $-prefixed keys across nested objects/arrays.
  • Added siteUrl and authProviders to the Project fields selected in verifyApiKey middleware.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
packages/common/src/utils/input.validation.js Makes sanitize() recursive to remove $ keys at all depths.
apps/public-api/src/middlewares/verifyApiKey.js Extends Project .select() projection to include siteUrl and authProviders.
apps/public-api/src/controllers/userAuth.controller.js Switches OTP generation to use Node crypto CSPRNG.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +49
siteUrl
authProviders
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Adding siteUrl/authProviders to this middleware’s Project projection may not resolve the social-auth redirect issue described in the PR: the social flow re-fetches the project via getSocialProviderConfig() (apps/public-api/src/controllers/userAuth.controller.js around the selectClause construction) and that select does not include siteUrl, which is what getFrontendCallbackBaseUrl() reads. Consider including siteUrl in the getSocialProviderConfig() projection (or reusing req.project there) so the callback URL is built from the actual project setting.

Copilot uses AI. Check for mistakes.
Comment on lines +293 to +294
for (const key in obj) {
if (!key.startsWith("$")) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

sanitize() now recursively copies keys into a fresh object, but it still allows special keys like __proto__, constructor, and prototype. If an attacker submits these keys in JSON payloads (e.g. in insert/update endpoints that call sanitize()), assigning them onto clean can mutate the object's prototype and lead to prototype-pollution style issues. Please explicitly block these keys (in addition to $-prefixed keys) and consider iterating only own-properties (e.g., Object.keys() / hasOwnProperty) when building clean.

Suggested change
for (const key in obj) {
if (!key.startsWith("$")) {
const blockedKeys = new Set(["__proto__", "constructor", "prototype"]);
for (const key of Object.keys(obj)) {
if (!key.startsWith("$") && !blockedKeys.has(key)) {

Copilot uses AI. Check for mistakes.
Comment on lines +970 to 972
const otp = crypto.randomInt(100000, 1000000).toString();
await redis.set(`project:${project._id}:otp:verification:${email}`, otp, 'EX', 300);
await setPublicOtpCooldown(project._id, email, 'verification');
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

OTP/cooldown Redis keys here are built with the raw email from the request, but verifyEmail later looks up the OTP using a normalized/lowercased email in the Redis key. If the user signs up with mixed-case or surrounding whitespace, the OTP will be stored under a different key and verification (and cooldown enforcement) can fail / be bypassed. Use the same normalized email value consistently for OTP storage and cooldown keys (and ideally for downstream email sending payloads too).

Copilot uses AI. Check for mistakes.
Comment on lines +995 to 996
const otp = crypto.randomInt(100000, 1000000).toString();

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

In this signup path, normalizedEmail is computed earlier but the OTP is later stored in Redis under a key that uses the raw email value (see the redis.set(...otp:verification:${email}...) a few lines below). This can break verification/cooldown when the input email contains uppercase or whitespace. Use the normalized email consistently for OTP storage and subsequent lookups.

Copilot uses AI. Check for mistakes.
Comment on lines 1306 to 1308
const otp = crypto.randomInt(100000, 1000000).toString();
await redis.set(`project:${project._id}:otp:verification:${email}`, otp, 'EX', 300);
await setPublicOtpCooldown(project._id, email, 'verification');
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Same issue as in signup: OTP/cooldown Redis keys use the raw email value, but the verification flow normalizes email before reading the OTP key. This can cause resend + verification to fail for mixed-case/whitespace emails and can allow cooldown bypass by changing email casing. Store OTP and set cooldown using the normalized email consistently.

Copilot uses AI. Check for mistakes.
- enforce correct api response formats
- add graceful shutdown for redis/bullmq
- fix email normalization on verify/resend
- remove dead code and noisy logs
- secure storage delete endpoints
@yash-pouranik yash-pouranik self-requested a review April 15, 2026 18:12
@yash-pouranik
Copy link
Copy Markdown
Collaborator

@prathamtagad
Thank you for the PR
Can you please Resolve Merge Conflicts??

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.

3 participants