Skip to content

🛡️ Sentinel: Fix IDOR and SQL Wildcard Enumeration vulnerabilities#524

Open
ngoiyaeric wants to merge 1 commit intomainfrom
sentinel-security-fixes-13716273341324520366
Open

🛡️ Sentinel: Fix IDOR and SQL Wildcard Enumeration vulnerabilities#524
ngoiyaeric wants to merge 1 commit intomainfrom
sentinel-security-fixes-13716273341324520366

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Feb 16, 2026

This PR addresses several security vulnerabilities in the application's server actions:

  1. Insecure Direct Object Reference (IDOR) in getChatMessages: Previously, any authenticated user could retrieve all messages for any chatId by calling the server action directly. I added a check that verifies the user has access to the chat (either as owner or if it's public) before returning messages.
  2. IDOR in updateDrawingContext: Authenticated users could add data messages to any chat. I added an ownership check to restrict this to the chat owner.
  3. SQL Wildcard Enumeration in searchUsers: The ilike search was susceptible to wildcard attacks (using % or _) which could allow a user to enumerate all user emails. I implemented escaping for these characters and added a 255-character length limit to the query.

Verified with static checks (lint/tsc) and manual code review. e2e tests were not runnable in the environment but the logic is defensive and safe.


PR created automatically by Jules for task 13716273341324520366 started by @ngoiyaeric

Summary by CodeRabbit

  • Documentation

    • Added security best practices documentation covering authentication and injection prevention.
  • Bug Fixes

    • Improved authentication and authorization enforcement for messaging and context features.
    • Added input validation and protection against injection attacks in search functionality.

…ties

Summary of changes:
1. Fix IDOR in `getChatMessages`: Added authentication and authorization checks to ensure users can only access their own or public chats.
2. Fix IDOR in `updateDrawingContext`: Added ownership check to ensure only the chat owner can add drawing context messages.
3. Fix SQL Wildcard Enumeration in `searchUsers`: Escaped special PostgreSQL characters (% , _, \) and added a length limit to the search query.
4. Added security learnings to `.jules/sentinel.md`.

These changes improve the security posture of the application by protecting private user data and preventing information disclosure.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
@vercel
Copy link
Contributor

vercel bot commented Feb 16, 2026

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

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment Feb 16, 2026 10:06am

@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Walkthrough

The changes add security hardening across multiple server actions by implementing authentication enforcement, authorization checks, and SQL injection prevention. Documentation is added detailing IDOR vulnerabilities and secure server action patterns with input validation guidelines.

Changes

Cohort / File(s) Summary
Security Documentation
.jules/sentinel.md
New security vulnerability documentation covering IDOR risks in chat functions, wildcard enumeration in user search, and best practices for Next.js server action authentication and SQL injection prevention.
Chat Action Security
lib/actions/chat.ts
getChatMessages now retrieves and validates the current user, returning empty list if unauthorized. updateDrawingContext enforces ownership checks before creating drawing messages, replacing unconditional message creation with auth-gated logic.
User Search Input Validation
lib/actions/users.ts
searchUsers now validates query length (max 255 chars), escapes PostgreSQL wildcard characters (%, _, \), and uses the escaped query in the ILIKE pattern to prevent SQL injection and abuse.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With whiskers twitched and paws so swift,
We guard the gates with every gift—
No SQL gremlins shall break through,
Auth checks and escapes, shiny and new! ✨
Security's burrow, now fortified true! 🔐

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 directly and accurately summarizes the main changes: fixing IDOR vulnerabilities in getChatMessages/updateDrawingContext and SQL wildcard enumeration in searchUsers.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sentinel-security-fixes-13716273341324520366

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
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/actions/chat.ts (1)

170-187: 🧹 Nitpick | 🔵 Trivial

Pre-existing IDOR: saveSystemPrompt and getSystemPrompt accept userId without authentication.

While out of scope for this PR, these functions accept a caller-supplied userId and perform DB operations without verifying that the authenticated user matches. This is the same class of IDOR vulnerability being fixed in getChatMessages and updateDrawingContext. The same pattern applies to clearChats (line 84), which uses the supplied userId directly.

Consider applying the same fix pattern (call getCurrentUserIdOnServer() and ignore/reject mismatched caller-supplied IDs) in a follow-up.

#!/bin/bash
# Check how saveSystemPrompt and getSystemPrompt are called — are caller-supplied userIds verified upstream?
rg -n --type=ts -C3 'saveSystemPrompt\|getSystemPrompt' -g '!node_modules' -g '!.next'
🤖 Fix all issues with AI agents
In @.jules/sentinel.md:
- Around line 1-4: Change the first line into a top-level heading by prefixing
it with "# " (e.g., "# 2026-02-16 - IDOR and Wildcard Enumeration in Server
Actions") and add blank lines immediately before and after each heading
(including the main heading and any "##" or "###" headings) so they are
surrounded by blank lines; also ensure the `<details>`/`<summary>` block is
separated from adjacent headings/content with blank lines to satisfy MD022 and
MD041.

In `@lib/actions/chat.ts`:
- Around line 60-72: The current behavior is inconsistent:
getCurrentUserIdOnServer() throws for unauthenticated but getChat(chatId,
userId) returns [] for unauthorized/DB errors; change to a consistent
return-of-empty-array policy by replacing the thrown Error from the
unauthenticated check with an early return of [] (instead of throw), keep the
existing try/catch around getChat, and ensure any logging (e.g., the
console.warn for unauthorized access) remains but does not change the return
contract; update callers/tests that assume an exception to expect
DrizzleMessage[] from this action.

In `@lib/actions/users.ts`:
- Around line 192-195: The input-length check currently throws an Error when
query.length > 255, causing inconsistent behavior because the empty-query path
returns [] earlier; update the logic in the same function that references query
(replace the throw new Error('Search query too long') branch) to return [] for
over-length queries instead of throwing so callers receive a uniform []
response, or alternatively add documentation/comments to the function explaining
that it will throw for too-long queries if you prefer keeping the throw (choose
one approach and apply it to the query length check).
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be08900 and 0780fed.

📒 Files selected for processing (3)
  • .jules/sentinel.md
  • lib/actions/chat.ts
  • lib/actions/users.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-10T08:45:03.719Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-02-10T08:45:03.719Z
Learning: The app/api/chats/route.ts endpoint correctly returns { chats: [] } for unauthenticated users instead of a 401 error. This is the intended behavior for better UX and should not be changed to return 401.

Applied to files:

  • lib/actions/chat.ts
🧬 Code graph analysis (3)
lib/actions/users.ts (3)
lib/auth/get-current-user.ts (1)
  • getCurrentUserIdOnServer (112-127)
lib/db/index.ts (1)
  • db (25-25)
lib/db/schema.ts (1)
  • users (22-28)
lib/actions/chat.ts (4)
lib/auth/get-current-user.ts (1)
  • getCurrentUserIdOnServer (112-127)
lib/actions/chat-db.ts (2)
  • getChat (22-40)
  • getMessagesByChatId (198-214)
components/chat.tsx (1)
  • id (96-104)
components/sidebar/chat-history-client.tsx (1)
  • ChatHistoryClientProps (26-28)
.jules/sentinel.md (1)
app/actions.tsx (2)
  • state (598-659)
  • clearChat (550-559)
🪛 markdownlint-cli2 (0.20.0)
.jules/sentinel.md

[warning] 1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🔇 Additional comments (2)
lib/actions/users.ts (1)

203-211: Wildcard escaping logic is correct and effectively mitigates enumeration.

The regex /[%_\\]/g correctly escapes all three PostgreSQL LIKE special characters in the right order (backslash first if encountered, then % and _).

One robustness note: this relies on PostgreSQL's default behavior of treating \ as the LIKE escape character. If you ever need to make this explicit, you'd use a raw SQL fragment with an ESCAPE '\' clause. For now, this is fine with standard PostgreSQL configurations.

lib/actions/chat.ts (1)

144-157: Correct ownership enforcement for drawing context updates.

The two-layer check is well designed: getChat verifies view-access (owner or public), and chat.userId !== userId further restricts mutation to the owner only. Moving the message construction (lines 151–157) after the authorization gate is the right fix — previously the payload was built before validation.

LGTM.

Comment on lines +1 to +4
## 2026-02-16 - IDOR and Wildcard Enumeration in Server Actions
**Vulnerability:** IDOR in `getChatMessages` and `updateDrawingContext` allowed unauthorized access/modification to chat data. `searchUsers` was susceptible to wildcard enumeration.
**Learning:** Exported server actions in Next.js ('use server') are public endpoints and must implement their own authentication and authorization checks, even if they are only intended for use in protected pages.
**Prevention:** Always retrieve the current user session within the server action and verify ownership/permissions before performing database operations. Escape special SQL characters in `LIKE`/`ILIKE` patterns from user input.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix markdown lint warnings (MD022, MD041).

Static analysis flags two issues: the first line should be a top-level heading (#), and headings need surrounding blank lines.

Proposed fix
-## 2026-02-16 - IDOR and Wildcard Enumeration in Server Actions
-**Vulnerability:** IDOR in `getChatMessages` and `updateDrawingContext` allowed unauthorized access/modification to chat data. `searchUsers` was susceptible to wildcard enumeration.
-**Learning:** Exported server actions in Next.js ('use server') are public endpoints and must implement their own authentication and authorization checks, even if they are only intended for use in protected pages.
-**Prevention:** Always retrieve the current user session within the server action and verify ownership/permissions before performing database operations. Escape special SQL characters in `LIKE`/`ILIKE` patterns from user input.
+# Sentinel Security Notes
+
+## 2026-02-16 - IDOR and Wildcard Enumeration in Server Actions
+
+**Vulnerability:** IDOR in `getChatMessages` and `updateDrawingContext` allowed unauthorized access/modification to chat data. `searchUsers` was susceptible to wildcard enumeration.
+
+**Learning:** Exported server actions in Next.js ('use server') are public endpoints and must implement their own authentication and authorization checks, even if they are only intended for use in protected pages.
+
+**Prevention:** Always retrieve the current user session within the server action and verify ownership/permissions before performing database operations. Escape special SQL characters in `LIKE`/`ILIKE` patterns from user input.
📝 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
## 2026-02-16 - IDOR and Wildcard Enumeration in Server Actions
**Vulnerability:** IDOR in `getChatMessages` and `updateDrawingContext` allowed unauthorized access/modification to chat data. `searchUsers` was susceptible to wildcard enumeration.
**Learning:** Exported server actions in Next.js ('use server') are public endpoints and must implement their own authentication and authorization checks, even if they are only intended for use in protected pages.
**Prevention:** Always retrieve the current user session within the server action and verify ownership/permissions before performing database operations. Escape special SQL characters in `LIKE`/`ILIKE` patterns from user input.
# Sentinel Security Notes
## 2026-02-16 - IDOR and Wildcard Enumeration in Server Actions
**Vulnerability:** IDOR in `getChatMessages` and `updateDrawingContext` allowed unauthorized access/modification to chat data. `searchUsers` was susceptible to wildcard enumeration.
**Learning:** Exported server actions in Next.js ('use server') are public endpoints and must implement their own authentication and authorization checks, even if they are only intended for use in protected pages.
**Prevention:** Always retrieve the current user session within the server action and verify ownership/permissions before performing database operations. Escape special SQL characters in `LIKE`/`ILIKE` patterns from user input.
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
In @.jules/sentinel.md around lines 1 - 4, Change the first line into a
top-level heading by prefixing it with "# " (e.g., "# 2026-02-16 - IDOR and
Wildcard Enumeration in Server Actions") and add blank lines immediately before
and after each heading (including the main heading and any "##" or "###"
headings) so they are surrounded by blank lines; also ensure the
`<details>`/`<summary>` block is separated from adjacent headings/content with
blank lines to satisfy MD022 and MD041.

Comment on lines +60 to +72

const userId = await getCurrentUserIdOnServer();
if (!userId) {
throw new Error('Unauthorized: Authentication required');
}

try {
// Check if the user has access to this chat
const chat = await getChat(chatId, userId);
if (!chat) {
console.warn(`Unauthorized access attempt to chat ${chatId} by user ${userId}`);
return [];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Good IDOR fix — but mixed error-handling contract may surprise callers.

The authentication and authorization checks correctly prevent unauthorized access to chat messages. However, the function has an inconsistent contract:

  • Unauthenticatedthrow (line 63), which escapes the try/catch at line 66.
  • Unauthorized (no chat access) → silently returns [] (line 71).
  • DB error → caught and returns [] (line 77).

A caller expecting DrizzleMessage[] will get an unhandled exception for unauthenticated requests but an empty array for everything else. Consider either returning [] consistently (aligning with the learned pattern that the chats API route returns empty for unauthenticated users) or throwing consistently.

Option A: return [] consistently (matches existing UX pattern)
   const userId = await getCurrentUserIdOnServer();
   if (!userId) {
-    throw new Error('Unauthorized: Authentication required');
+    console.warn('getChatMessages: User not authenticated');
+    return [];
   }

Based on learnings, the app/api/chats/route.ts endpoint returns { chats: [] } for unauthenticated users instead of a 401 — a similar pattern here would keep the contract uniform.

🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 60 - 72, The current behavior is
inconsistent: getCurrentUserIdOnServer() throws for unauthenticated but
getChat(chatId, userId) returns [] for unauthorized/DB errors; change to a
consistent return-of-empty-array policy by replacing the thrown Error from the
unauthenticated check with an early return of [] (instead of throw), keep the
existing try/catch around getChat, and ensure any logging (e.g., the
console.warn for unauthorized access) remains but does not change the return
contract; update callers/tests that assume an exception to expect
DrizzleMessage[] from this action.

Comment on lines +192 to +195
// Prevent DoS and long pattern matching
if (query.length > 255) {
throw new Error('Search query too long');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good addition of input length validation.

The length check correctly guards against excessively long patterns. One minor inconsistency: an empty query silently returns [] (line 190), but an over-length query throws. Consider returning [] here as well for a uniform caller experience, or document the throwing behavior so callers know to handle it.

🤖 Prompt for AI Agents
In `@lib/actions/users.ts` around lines 192 - 195, The input-length check
currently throws an Error when query.length > 255, causing inconsistent behavior
because the empty-query path returns [] earlier; update the logic in the same
function that references query (replace the throw new Error('Search query too
long') branch) to return [] for over-length queries instead of throwing so
callers receive a uniform [] response, or alternatively add
documentation/comments to the function explaining that it will throw for
too-long queries if you prefer keeping the throw (choose one approach and apply
it to the query length check).

Copy link

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

The main risk is in searchUsers: escaping wildcards may be ineffective unless the generated SQL includes ESCAPE '\\', which could leave wildcard enumeration partially unresolved. Additionally, getChatMessages mixes throw-vs-empty-array behavior for auth failures, making callers’ error handling inconsistent. Finally, the new authz warning logs include potentially sensitive identifiers; consider redaction to reduce exposure.

Additional notes (3)
  • Maintainability | lib/actions/chat.ts:57-76
    getChatMessages currently returns an empty array on unauthorized access, which is indistinguishable from “chat exists but has no messages”. That’s good for reducing information disclosure, but the function throws for unauthenticated callers while silently returning [] for unauthorized callers. This inconsistency can lead to confusing client behavior and makes security posture harder to reason about across actions (some callers will need try/catch, others won’t).

Given this is a server action used by UI code, decide on a single contract:

  • either always throw on auth/authz failures, or
  • always return a structured { error } result.

Mixing the two tends to create edge cases and can accidentally reintroduce side-channels depending on how the UI reacts to thrown vs non-thrown failures.

  • Security | lib/actions/chat.ts:140-161
    updateDrawingContext returns { error: 'User not authenticated' } for unauthenticated users but returns { error: 'Unauthorized: ...' } for non-owners. That’s fine, but you’re also logging the chatId and userId in warning logs. If logs are accessible broadly (or shipped to third parties), this can leak sensitive identifiers.

Consider reducing identifier exposure in logs (e.g., omit userId, or hash/truncate), or log only at debug level with a request correlation ID.

  • Maintainability | lib/actions/users.ts:189-195
    The new length limit throws Error('Search query too long'), while other failure paths return [] (catch) or throw Unauthorized. This inconsistency is likely to bubble up as a 500 in some callers and a normal empty result in others. For user search, it’s generally better to return a deterministic client-friendly response (e.g., []) for invalid input, or a structured error shape, rather than throwing a generic error.
Summary of changes

Security hardening for server actions

  • Added authorization checks to getChatMessages(chatId) by fetching the current user via getCurrentUserIdOnServer() and verifying access via getChat(chatId, userId) before returning messages.
  • Restricted updateDrawingContext(chatId, contextData) to chat owners only, moving message creation inside the authorization-guarded try block.
  • Mitigated wildcard enumeration in searchUsers(query) by:
    • enforcing a 255 character max query length,
    • escaping PostgreSQL pattern characters (%, _, \) prior to ilike(users.email, ...).
  • Added a security learnings note in .jules/sentinel.md documenting IDOR/wildcard enumeration prevention lessons.

@charliecreates charliecreates bot removed the request for review from CharlieHelps February 16, 2026 10:09
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.

2 participants