From 0780fed914e1279dcd0383e30857807bd53b2880 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:05:17 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Fi?= =?UTF-8?q?x=20IDOR=20and=20SQL=20Wildcard=20Enumeration=20vulnerabilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .jules/sentinel.md | 4 ++++ lib/actions/chat.ts | 36 ++++++++++++++++++++++++++++-------- lib/actions/users.ts | 10 +++++++++- 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..18114834 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,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. diff --git a/lib/actions/chat.ts b/lib/actions/chat.ts index f36f2cf6..d0151e92 100644 --- a/lib/actions/chat.ts +++ b/lib/actions/chat.ts @@ -57,7 +57,20 @@ export async function getChatMessages(chatId: string): Promise console.warn('getChatMessages called without chatId'); return []; } + + 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 []; + } + return dbGetMessagesByChatId(chatId); } catch (error) { console.error(`Error fetching messages for chat ${chatId} in getChatMessages:`, error); @@ -127,15 +140,22 @@ export async function updateDrawingContext(chatId: string, contextData: { drawnF return { error: 'User not authenticated' }; } - const newDrawingMessage: DbNewMessage = { - userId: userId, - chatId: chatId, - role: 'data', - content: JSON.stringify(contextData), - createdAt: new Date(), - }; - try { + // Check if the user has access to this chat and owns it + const chat = await getChat(chatId, userId); + if (!chat || chat.userId !== userId) { + console.warn(`Unauthorized drawing context update attempt for chat ${chatId} by user ${userId}`); + return { error: 'Unauthorized: Ownership required to update drawing context' }; + } + + const newDrawingMessage: DbNewMessage = { + userId: userId, + chatId: chatId, + role: 'data', + content: JSON.stringify(contextData), + createdAt: new Date(), + }; + const savedMessage = await dbCreateMessage(newDrawingMessage); if (!savedMessage) { throw new Error('Failed to save drawing context message.'); diff --git a/lib/actions/users.ts b/lib/actions/users.ts index 6fba7f8b..9ccab2c4 100644 --- a/lib/actions/users.ts +++ b/lib/actions/users.ts @@ -189,18 +189,26 @@ export async function searchUsers(query: string) { noStore(); if (!query) return []; + // Prevent DoS and long pattern matching + if (query.length > 255) { + throw new Error('Search query too long'); + } + const userId = await getCurrentUserIdOnServer(); if (!userId) { throw new Error('Unauthorized'); } try { + // Escape special PostgreSQL characters to prevent wildcard enumeration + const escapedQuery = query.replace(/[%_\\]/g, '\\$&'); + const result = await db.select({ id: users.id, email: users.email, }) .from(users) - .where(ilike(users.email, `%${query}%`)) + .where(ilike(users.email, `%${escapedQuery}%`)) .limit(10); return result;