-
-
Notifications
You must be signed in to change notification settings - Fork 6
🛡️ Sentinel: Fix IDOR and SQL Wildcard Enumeration vulnerabilities #524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,7 +57,20 @@ export async function getChatMessages(chatId: string): Promise<DrizzleMessage[]> | |
| 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 []; | ||
| } | ||
|
Comment on lines
+60
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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:
A caller expecting 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 🤖 Prompt for AI Agents |
||
|
|
||
| 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.'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'); | ||
| } | ||
|
Comment on lines
+192
to
+195
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤖 Prompt for AI Agents |
||
|
|
||
| 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; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
📝 Committable suggestion
🧰 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