Skip to content

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

Open
Kirtan-pc wants to merge 3 commits into
geturbackend:mainfrom
Kirtan-pc:fix/allowed-domains-cache-invalidation
Open

fix(cache): invalidate both raw and hashed publishable keys and encrypt jwtSecret in Redis#342
Kirtan-pc wants to merge 3 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

  • Bug Fixes

    • Improved Redis caching for project lookups by correctly encrypting sensitive secret values before storing and decrypting them on retrieval.
    • Enhanced cache invalidation during API key verification to handle stale entries more reliably, including safer handling for previously cached (plaintext) data.
    • Strengthened API key verification behavior when cached data contains null/invalid secrets, and ensured consistent project enrichment and validation.
  • Tests

    • Added Jest coverage for Redis caching encryption/decryption and correct deletion behavior across hashed and non-hashed cache entries.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Redis caching now encrypts jwtSecret on write and decrypts it on read, API-key deletion uses hashed live keys, and verifyApiKey invalidates stale cached projects before applying updated project and origin checks. A new Jest suite covers the Redis helper behavior.

Changes

Redis Cache Security Fixes

Layer / File(s) Summary
Encrypt/decrypt jwtSecret and hash API-key deletion
packages/common/src/redis/redisCaching.js
setProjectByApiKeyCache and setProjectById clone project and encrypt string jwtSecret values before JSON storage; getProjectByApiKeyCache and getProjectById decrypt parsed encrypted jwtSecret values on read; deleteProjectByApiKeyCache hashes live API keys before deleting the cache entry.
Invalidate stale cache and apply project checks
apps/public-api/src/middlewares/verifyApiKey.js, pr_description.md
verifyApiKey adds cache invalidation for cached projects with jwtSecret === null, tightens query-key handling to pk_live_ values, deletes req.query.key, and updates post-fetch handling for missing projects, cache writes, verified owners, resource defaults, and origin allowlisting. The PR description records the cache invalidation and jwtSecret encryption changes.
Redis caching tests
apps/public-api/src/__tests__/redisCaching.test.js
The new Jest suite mocks Redis with an in-memory Map, fixes ENCRYPTION_KEY, and verifies encrypted writes, decrypted reads, plaintext fallback, and raw-versus-hashed cache deletion behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • yash-pouranik

Poem

🐇 I hopped through Redis, quick as a breeze,
Encrypting secrets and hashing the keys.
Old cache entries? Poof, they’re gone,
Fresh checks and safe secrets carry on.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning verifyApiKey.js includes unrelated auth and request-shaping changes like owner verification, resource defaults, and broader origin handling. Remove or justify the unrelated middleware changes and keep the PR focused on cache invalidation and jwtSecret protection.
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 clearly summarizes the two main cache-related fixes: invalidation for publishable keys and encrypting jwtSecret in Redis.
Linked Issues check ✅ Passed The changes address the linked security issue by invalidating cached publishable-key entries and encrypting/decrypting jwtSecret with a plaintext fallback.
✨ 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

// a publishable key may be supplied via the `key` query parameter instead.
const headerKey = req.header("x-api-key");
const queryKey =
typeof req.query?.key === "string" ? req.query.key : undefined;
@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 on lines +102 to +110
const cacheKey = isSecret
? hashedApi
: project[keyField] === apiKey
? apiKey
: hashedApi;
await setProjectByApiKeyCache(cacheKey, project);
}

if (!project) {
return next(new AppError(401, 'Please use a valid API key or regenerate a new one from the dashboard.', 'API key is expired or invalid.'));
}
if (!project.owner.isVerified) {

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Move the cache write after owner verification.

Line 107 caches the project before Line 110 rejects unverified owners, so a request made before verification can store owner.isVerified: false and keep rejecting from Redis until the cache expires. Defer setProjectByApiKeyCache until after the owner check.

Proposed fix
+    let cacheKey;
+
     if (!project) {
@@
-      const cacheKey = isSecret
+      cacheKey = isSecret
         ? hashedApi
         : project[keyField] === apiKey
           ? apiKey
           : hashedApi;
-      await setProjectByApiKeyCache(cacheKey, project);
     }
 
-    if (!project.owner.isVerified) {
+    if (!project.owner?.isVerified) {
       return next(
         new AppError(
           401,
@@
       );
     }
+
+    if (cacheKey) {
+      await setProjectByApiKeyCache(cacheKey, project);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cacheKey = isSecret
? hashedApi
: project[keyField] === apiKey
? apiKey
: hashedApi;
await setProjectByApiKeyCache(cacheKey, project);
}
if (!project) {
return next(new AppError(401, 'Please use a valid API key or regenerate a new one from the dashboard.', 'API key is expired or invalid.'));
}
if (!project.owner.isVerified) {
let cacheKey;
cacheKey = isSecret
? hashedApi
: project[keyField] === apiKey
? apiKey
: hashedApi;
}
if (!project.owner?.isVerified) {
return next(
new AppError(
401,
...
)
);
}
if (cacheKey) {
await setProjectByApiKeyCache(cacheKey, project);
}
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 106-106: Avoid using the initial state variable in setState
Context: setProjectByApiKeyCache(cacheKey, project)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 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/middlewares/verifyApiKey.js` around lines 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.

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.

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

3 participants