From fc3e07a50cb5c722d9f1a986b77e86ff9ef1a1ad Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:27:05 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20mass?= =?UTF-8?q?=20assignment=20and=20wildcard=20enumeration=20vulnerabilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix mass assignment in `lib/actions/calendar.ts` by explicitly listing mutable columns in `saveNote`. - Fix wildcard enumeration in `lib/actions/users.ts` by escaping `ILIKE` special characters and enforcing length limits in `searchUsers`. - Add Sentinel Security Journal with critical learnings. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .jules/sentinel.md | 11 +++++++++++ lib/actions/calendar.ts | 20 ++++++++++++++++++-- lib/actions/users.ts | 10 +++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..bedc8d0e --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,11 @@ +# Sentinel Security Journal + +## 2025-05-14 - [Vulnerability] Mass Assignment in Drizzle Server Actions +**Vulnerability:** Use of the spread operator (`...noteData`) in Drizzle `.set()` or `.values()` calls allowed client-supplied input to overwrite protected fields like `userId`. +**Learning:** Even if the `where` clause checks for the correct `userId`, mass assignment can still allow a user to "transfer" their records to another user by changing the `userId` in the payload. +**Prevention:** Always explicitly list mutable columns in database operations within server actions. + +## 2025-05-14 - [Vulnerability] Wildcard Enumeration in ILIKE Queries +**Vulnerability:** Unsanitized user input in `ILIKE` patterns (`%${query}%`) allowed authenticated users to enumerate records (e.g., all user emails) by using `%` or `_` wildcards. +**Learning:** Drizzle's `ilike` operator does not automatically escape wildcards. Postgres requires an explicit `ESCAPE` clause to treat these characters as literals. +**Prevention:** Sanitize user input by escaping `%`, `_`, and `\` and using `sql`${column} ILIKE ${pattern} ESCAPE '\\'` template literals. diff --git a/lib/actions/calendar.ts b/lib/actions/calendar.ts index d2e4dcf9..d3d9c81e 100644 --- a/lib/actions/calendar.ts +++ b/lib/actions/calendar.ts @@ -73,7 +73,15 @@ export async function saveNote(noteData: NewCalendarNote | CalendarNote): Promis try { const [updatedNote] = await db .update(calendarNotes) - .set({ ...noteData, updatedAt: new Date() }) + .set({ + content: noteData.content, + date: noteData.date, + chatId: noteData.chatId, + locationTags: noteData.locationTags, + userTags: noteData.userTags, + mapFeatureId: noteData.mapFeatureId, + updatedAt: new Date() + }) .where(and(eq(calendarNotes.id, noteData.id), eq(calendarNotes.userId, userId))) .returning(); return updatedNote; @@ -86,7 +94,15 @@ export async function saveNote(noteData: NewCalendarNote | CalendarNote): Promis try { const [newNote] = await db .insert(calendarNotes) - .values({ ...noteData, userId }) + .values({ + userId, + content: noteData.content, + date: noteData.date, + chatId: noteData.chatId, + locationTags: noteData.locationTags, + userTags: noteData.userTags, + mapFeatureId: noteData.mapFeatureId, + }) .returning(); if (newNote && newNote.chatId) { diff --git a/lib/actions/users.ts b/lib/actions/users.ts index 6fba7f8b..5bce6fff 100644 --- a/lib/actions/users.ts +++ b/lib/actions/users.ts @@ -3,7 +3,7 @@ import { revalidatePath, unstable_noStore as noStore } from 'next/cache'; import { db } from '@/lib/db'; import { users } from '@/lib/db/schema'; -import { eq, ilike } from 'drizzle-orm'; +import { eq, ilike, sql } from 'drizzle-orm'; import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; export type UserRole = "admin" | "editor" | "viewer"; @@ -187,7 +187,8 @@ export async function saveSelectedModel(model: string): Promise<{ success: boole */ export async function searchUsers(query: string) { noStore(); - if (!query) return []; + // Enforce length limit and handle empty query to prevent DoS/enumeration + if (!query || query.length > 100) return []; const userId = await getCurrentUserIdOnServer(); if (!userId) { @@ -195,12 +196,15 @@ export async function searchUsers(query: string) { } try { + // Escape special ILIKE 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(sql`${users.email} ILIKE ${'%' + escapedQuery + '%'} ESCAPE '\\'`) .limit(10); return result;