Skip to content

fix(cache): invalidate both raw and hashed publishable keys and encrypt jwtSecret in Redis - #342

Merged
yash-pouranik merged 7 commits into
geturbackend:mainfrom
Kirtan-pc:fix/allowed-domains-cache-invalidation
Jul 10, 2026
Merged

fix(cache): invalidate both raw and hashed publishable keys and encrypt jwtSecret in Redis#342
yash-pouranik merged 7 commits into
geturbackend:mainfrom
Kirtan-pc:fix/allowed-domains-cache-invalidation

Conversation

@Kirtan-pc

@Kirtan-pc Kirtan-pc commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🚀 Pull Request Description

Fixes #286

This PR resolves two security and consistency issues related to project caching in Redis:

  1. Cache Invalidation Gap: Corrects cache invalidation inside deleteProjectByApiKeyCache. When updating allowedDomains in the dashboard, the cache was not properly invalidated for publishable keys because the key was stored under the hashed value of the key (since verifyApiKey.js did not select publishableKey from the DB, resulting in it checking undefined === apiKey and caching it under the hashedApi). deleteProjectByApiKeyCache now deletes both raw and hashed versions of keys to guarantee proper invalidation.
  2. Plaintext Secrets: Encrypts the sensitive jwtSecret field prior to writing to the Redis cache and decrypts it when retrieving. A graceful fallback is included to support reading existing plaintext entries without breaking active sessions.

🛠️ Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🎨 UI/UX improvement (Frontend only)
  • ⚙️ Refactor / Chore

🧪 Testing & Validation

Backend Verification:

  • I have run npm test in the backend/ directory and all tests passed.
  • I have verified the API endpoints using Postman/Thunder Client.
  • New unit tests have been added (if applicable).

Frontend Verification:

  • I have run npm run lint in the frontend/ directory.
  • Verified the UI changes on different screen sizes (Responsive).
  • Checked for any console errors in the browser dev tools.

📸 Screenshots / Recordings (Optional)

✅ Checklist

  • My code follows the code style of this project.
  • I have performed a self-review of my code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings or errors.
  • I have updated the documentation (README/Docs) accordingly.

Built with ❤️ for urBackend.

Summary by CodeRabbit

  • Security Enhancements

    • Redis-cached project secrets are now encrypted and securely decrypted when retrieved.
    • Invalid or stale cache entries are automatically removed and refreshed.
    • API key validation now supports origin and domain allowlists, including wildcard domains.
    • Unverified project owners are denied access and their projects are not cached.
  • Bug Fixes

    • Improved handling of raw and hashed API key cache deletion.
    • Query-string API keys are restricted to publishable keys and removed after processing.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@yash-pouranik, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d686795-7120-4759-8581-b2636d04cbcc

📥 Commits

Reviewing files that changed from the base of the PR and between fc852f9 and edde761.

📒 Files selected for processing (1)
  • packages/common/src/redis/redisCaching.js
📝 Walkthrough

Walkthrough

Redis project caching now encrypts jwtSecret, supports decryption and plaintext migration reads, and hashes live API-key cache deletions. verifyApiKey invalidates stale entries and applies updated key, owner, resource, and origin validation. Tests cover cache behavior and unverified owners.

Changes

Redis and API key security

Layer / File(s) Summary
Encrypt cached secrets and hash cache deletions
packages/common/src/redis/redisCaching.js
Project cache writes encrypt jwtSecret, reads decrypt it, and live API-key deletion targets hashed cache keys.
Invalidate stale cache and validate API keys
apps/public-api/src/middlewares/verifyApiKey.js
API-key extraction, stale-cache invalidation, project lookup, owner checks, resource defaults, origin allowlisting, and request metadata handling are updated.
Validate cache and middleware behavior
apps/public-api/src/__tests__/redisCaching.test.js, apps/public-api/src/__tests__/verifyApiKey.test.js
Tests cover encrypted cache writes, decrypted and plaintext-compatible reads, deletion variants, and avoiding cache writes for unverified owners.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant verifyApiKey
  participant Redis
  participant Project
  Client->>verifyApiKey: Send API key
  verifyApiKey->>Redis: Read cached project
  verifyApiKey->>Redis: Invalidate stale entry when jwtSecret is null
  verifyApiKey->>Project: Query project
  Project-->>verifyApiKey: Return project
  verifyApiKey->>Redis: Cache project with encrypted jwtSecret
  verifyApiKey-->>Client: Continue or return error
Loading

Possibly related PRs

Suggested reviewers: yash-pouranik

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The middleware also adds owner verification, resource defaults, and URL allowlist handling that are not required by the linked issue. Split those middleware behavior changes into a separate PR unless another issue explicitly requires them.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the cache invalidation and Redis jwtSecret encryption changes.
Linked Issues check ✅ Passed The Redis cache helpers now encrypt jwtSecret and delete hashed publishable-key entries, addressing the stale allowedDomains and plaintext-secret issues.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pr_description.md (1)

33-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add inline comments to the encryption and cache invalidation logic.

The PR checklist claims comments were added to hard-to-understand areas, but redisCaching.js lacks explanatory comments for:

  • Why jwtSecret is encrypted before caching
  • The plaintext fallback mechanism in the get helpers
  • Why both raw and hashed API keys are deleted

Consider adding brief inline comments to these security-sensitive code paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pr_description.md` at line 33, Add brief inline comments in redisCaching.js
around the security-sensitive paths that the checklist refers to: explain why
jwtSecret is encrypted before being cached, note the plaintext fallback behavior
in the get helper methods, and clarify why both raw and hashed API keys are
deleted during invalidation. Use the relevant helper functions in
redisCaching.js so the comments stay attached to the exact logic that future
readers will inspect.
apps/public-api/src/__tests__/redisCaching.test.js (1)

49-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert cache writes don’t mutate the input project.

The production helpers clone before encrypting; add assertions so a future regression that encrypts the caller’s object is caught.

Proposed test addition
         await setProjectByApiKeyCache('pk_live_123', project);
+        expect(project.jwtSecret).toBe('super-secret-jwt-key');
         await setProjectById('project_id_1', project);
+        expect(project.jwtSecret).toBe('id-secret');

Also applies to: 90-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/public-api/src/__tests__/redisCaching.test.js` at line 49, The cache
write test around setProjectByApiKeyCache should verify the input project object
is not mutated by encryption or caching. Update the assertions in
redisCaching.test.js to capture the original project state before calling
setProjectByApiKeyCache and confirm the caller’s object remains unchanged
afterward, matching the cloning behavior used by the production helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/common/src/redis/redisCaching.js`:
- Around line 46-48: The cached project reader in redisCaching.js is treating a
failed jwtSecret decrypt as a valid cached value; update the jwtSecret handling
in the relevant cache read paths (including the shared logic used around
parsedData.jwtSecret) so that when decrypt returns null the entry is treated as
a cache miss and the project is refetched instead of returning jwtSecret: null.
Make sure both affected reader locations use the same behavior and only return
cached data when jwtSecret decrypt succeeds.

---

Nitpick comments:
In `@apps/public-api/src/__tests__/redisCaching.test.js`:
- Line 49: The cache write test around setProjectByApiKeyCache should verify the
input project object is not mutated by encryption or caching. Update the
assertions in redisCaching.test.js to capture the original project state before
calling setProjectByApiKeyCache and confirm the caller’s object remains
unchanged afterward, matching the cloning behavior used by the production
helpers.

In `@pr_description.md`:
- Line 33: Add brief inline comments in redisCaching.js around the
security-sensitive paths that the checklist refers to: explain why jwtSecret is
encrypted before being cached, note the plaintext fallback behavior in the get
helper methods, and clarify why both raw and hashed API keys are deleted during
invalidation. Use the relevant helper functions in redisCaching.js so the
comments stay attached to the exact logic that future readers will inspect.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f33d64fe-fc14-4491-8d5f-ba4bdfe8e216

📥 Commits

Reviewing files that changed from the base of the PR and between da79159 and fbf4aa0.

📒 Files selected for processing (3)
  • apps/public-api/src/__tests__/redisCaching.test.js
  • packages/common/src/redis/redisCaching.js
  • pr_description.md

Comment on lines +46 to +48
if (parsedData && parsedData.jwtSecret && typeof parsedData.jwtSecret === "object") {
parsedData.jwtSecret = decrypt(parsedData.jwtSecret);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Treat failed jwtSecret decrypts as cache misses.

decrypt returns null on tamper/wrong-key failures, but these readers currently return a cached project with jwtSecret: null. That can break JWT validation until the Redis TTL expires instead of refetching a valid project.

Proposed fix
-        if (parsedData && parsedData.jwtSecret && typeof parsedData.jwtSecret === "object") {
-            parsedData.jwtSecret = decrypt(parsedData.jwtSecret);
-        }
+        if (parsedData && parsedData.jwtSecret && typeof parsedData.jwtSecret === "object") {
+            const decryptedJwtSecret = decrypt(parsedData.jwtSecret);
+            if (decryptedJwtSecret === null) {
+                await redis.del(`project:apikey:${api}`);
+                return null;
+            }
+            parsedData.jwtSecret = decryptedJwtSecret;
+        }
-        if (parsedData && parsedData.jwtSecret && typeof parsedData.jwtSecret === "object") {
-            parsedData.jwtSecret = decrypt(parsedData.jwtSecret);
-        }
+        if (parsedData && parsedData.jwtSecret && typeof parsedData.jwtSecret === "object") {
+            const decryptedJwtSecret = decrypt(parsedData.jwtSecret);
+            if (decryptedJwtSecret === null) {
+                await redis.del(`project:id:${id}`);
+                return null;
+            }
+            parsedData.jwtSecret = decryptedJwtSecret;
+        }

Also applies to: 112-114

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/redis/redisCaching.js` around lines 46 - 48, The cached
project reader in redisCaching.js is treating a failed jwtSecret decrypt as a
valid cached value; update the jwtSecret handling in the relevant cache read
paths (including the shared logic used around parsedData.jwtSecret) so that when
decrypt returns null the entry is treated as a cache miss and the project is
refetched instead of returning jwtSecret: null. Make sure both affected reader
locations use the same behavior and only return cached data when jwtSecret
decrypt succeeds.

@yash-pouranik

Copy link
Copy Markdown
Member

please fix coderabbits comments
@Kirtan-pc

Comment thread apps/public-api/src/middlewares/verifyApiKey.js Dismissed
@Kirtan-pc

Copy link
Copy Markdown
Contributor Author

Hey @yash-pouranik,
I have fixed the coderabbit comments. Please review it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/public-api/src/middlewares/verifyApiKey.js`:
- Around line 102-110: The cache write in verifyApiKey is happening before the
owner verification check, which can persist an unverified project state in
Redis. Move the setProjectByApiKeyCache call in verifyApiKey.js to after the
!project.owner.isVerified guard so only verified projects are cached, and keep
the existing cacheKey selection logic intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f2f7611-c8de-4f02-99d5-9c61d96ed874

📥 Commits

Reviewing files that changed from the base of the PR and between fbf4aa0 and bdc9a2d.

📒 Files selected for processing (1)
  • apps/public-api/src/middlewares/verifyApiKey.js

Comment thread apps/public-api/src/middlewares/verifyApiKey.js Outdated
@yash-pouranik

Copy link
Copy Markdown
Member

@Kirtan-pc
One more comment

@yash-pouranik

Copy link
Copy Markdown
Member

@Kirtan-pc
waiting.

@Kirtan-pc

Copy link
Copy Markdown
Contributor Author

Hey @yash-pouranik,
Apologies for the delay, I have fixed the coderabbit comments again, please review it.

Comment thread pr_description.md Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please remove this file
@Kirtan-pc

@Kirtan-pc

Copy link
Copy Markdown
Contributor Author

Hey @yash-pouranik,
done

Handle decryption failure for jwtSecret in cache.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/common/src/redis/redisCaching.js (1)

47-53: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Delete the actual cache key after decryption failure.

Line 49 references undeclared cacheKey, so cleanup throws and the corrupted entry remains cached.

Proposed fix
-              await redis.del(cacheKey);
+              await redis.del(`project:apikey:${api}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/redis/redisCaching.js` around lines 47 - 53, Replace the
undeclared cacheKey reference in the decryption-failure branch of the relevant
Redis cache retrieval function with the actual cache-key variable used when
reading the entry, then delete that key before returning null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/common/src/redis/redisCaching.js`:
- Around line 47-53: Replace the undeclared cacheKey reference in the
decryption-failure branch of the relevant Redis cache retrieval function with
the actual cache-key variable used when reading the entry, then delete that key
before returning null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 84d2c29a-af99-4165-9857-cb9d380329a7

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa7b36 and fc852f9.

📒 Files selected for processing (1)
  • packages/common/src/redis/redisCaching.js

@yash-pouranik
yash-pouranik merged commit ee48924 into geturbackend:main Jul 10, 2026
8 checks passed
@yash-pouranik

Copy link
Copy Markdown
Member

Done
Thank you
@Kirtan-pc

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] verifyApiKey: cached project document includes sensitive fields — cache bypass exposes stale allowedDomains

3 participants