fix(security): Update cookie parsing and user status handling
- Fix Cloudflare clearance cookie parsing regex - Normalize lastSeen timestamp to prevent null value issues - Improve user status timeout and offline status update logic - Add null coalescing for lastSeen to ensure consistent type handling
- commit
- 3a9c4bf4ae544de0bb76807e6ea9033fb945de85
- author
- Kit · · 2025-02-18 07:19 UTC
- parent
- 059b851
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
3 files · +40 -22
M src/lib/types/chat.ts +1 -1
| @@ -39,7 +39,7 @@ export type UserStatus = 'offline' | 'online' | 'away' | string; | |||
| 39 | 39 | nickname: user.nickname, | |
| 40 | 40 | status: user.status, | |
| 41 | 41 | avatarUrl: user.avatarUrl, | |
| 42 | - | lastSeen: user.lastSeen | |
| 42 | + | lastSeen: user.lastSeen ?? 0 | |
| 43 | 43 | }; | |
| 44 | 44 | return safeUser; | |
| 45 | 45 | } | |
| @@ -39,7 +39,7 @@ export type UserStatus = 'offline' | 'online' | 'away' | string; | |||||
| 39 | nickname: user.nickname, | 39 | nickname: user.nickname, | ||
| 40 | status: user.status, | 40 | status: user.status, | ||
| 41 | avatarUrl: user.avatarUrl, | 41 | avatarUrl: user.avatarUrl, | ||
| 42 | - | lastSeen: user.lastSeen | 42 | + | lastSeen: user.lastSeen ?? 0 |
| 43 | }; | 43 | }; | ||
| 44 | return safeUser; | 44 | return safeUser; | ||
| 45 | } | 45 | } | ||
M src/routes/api/rooms/[roomId]/+server.ts +37 -20
| @@ -24,42 +24,59 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 24 | 24 | // Calculate timeout threshold | |
| 25 | 25 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 26 | 26 | ||
| 27 | - | // Fetch buddy list: get all users and mark them as offline if they've timed out | |
| 27 | + | // Fetch buddy list: get all users | |
| 28 | 28 | const fetchedUsers = await db.select() | |
| 29 | 29 | .from(users) | |
| 30 | 30 | .execute(); | |
| 31 | 31 | ||
| 32 | - | // Process users to mark them as offline if they've timed out | |
| 33 | - | const processedUsers = fetchedUsers.map((user: SafeUser & { lastSeen: number }) => { | |
| 34 | - | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | |
| 35 | - | return { | |
| 32 | + | // Normalize users to ensure that lastSeen is always a number (using 0 if null) | |
| 33 | + | const normalizedUsers = fetchedUsers.map( | |
| 34 | + | (user: SafeUser & { lastSeen: number | null }) => ({ | |
| 36 | 35 | ...user, | |
| 37 | - | status: shouldBeOffline ? 'offline' : user.status | |
| 38 | - | }; | |
| 39 | - | }); | |
| 36 | + | lastSeen: user.lastSeen ?? 0 | |
| 37 | + | }) | |
| 38 | + | ); | |
| 40 | 39 | ||
| 41 | - | // Update any users that should be offline in the database | |
| 42 | - | const usersToUpdate = processedUsers.filter((user: SafeUser & { lastSeen: number }) => | |
| 43 | - | user.status === 'offline' && | |
| 44 | - | fetchedUsers.find((u: SafeUser & { lastSeen: number }) => u.id === user.id)?.status === 'online' | |
| 40 | + | // Process users to mark them as offline if they've timed out | |
| 41 | + | const processedUsers = normalizedUsers.map( | |
| 42 | + | (user: SafeUser & { lastSeen: number }) => { | |
| 43 | + | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | |
| 44 | + | return { | |
| 45 | + | ...user, | |
| 46 | + | status: shouldBeOffline ? 'offline' : user.status | |
| 47 | + | }; | |
| 48 | + | } | |
| 49 | + | ); | |
| 50 | + | ||
| 51 | + | // Find users who were originally online but now marked offline | |
| 52 | + | const usersToUpdate = processedUsers.filter( | |
| 53 | + | (user: SafeUser & { lastSeen: number }) => | |
| 54 | + | user.status === 'offline' && | |
| 55 | + | normalizedUsers.find( | |
| 56 | + | (u: SafeUser & { lastSeen: number }) => u.id === user.id | |
| 57 | + | )?.status === 'online' | |
| 45 | 58 | ); | |
| 46 | 59 | ||
| 47 | 60 | if (usersToUpdate.length > 0) { | |
| 48 | - | await Promise.all(usersToUpdate.map((user: SafeUser & { lastSeen: number }) => | |
| 49 | - | db.update(users) | |
| 50 | - | .set({ status: 'offline', lastSeen: Date.now() }) | |
| 51 | - | .where(eq(users.id, user.id)) | |
| 52 | - | .execute() | |
| 53 | - | )); | |
| 61 | + | await Promise.all( | |
| 62 | + | usersToUpdate.map((user: SafeUser & { lastSeen: number }) => | |
| 63 | + | db.update(users) | |
| 64 | + | .set({ status: 'offline', lastSeen: Date.now() }) | |
| 65 | + | .where(eq(users.id, user.id)) | |
| 66 | + | .execute() | |
| 67 | + | ) | |
| 68 | + | ); | |
| 54 | 69 | ||
| 55 | 70 | log.debug('Updated offline status for users', { | |
| 56 | 71 | count: usersToUpdate.length, | |
| 57 | 72 | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) | |
| 58 | 73 | }); | |
| 59 | 74 | } | |
| 60 | - | ||
| 75 | + | ||
| 61 | 76 | // Sanitize user data using createSafeUser | |
| 62 | - | const sanitizedUsers = processedUsers.map((user: SafeUser & { lastSeen: number }) => createSafeUser(user)); | |
| 77 | + | const sanitizedUsers = processedUsers.map( | |
| 78 | + | (user: SafeUser & { lastSeen: number }) => createSafeUser(user) | |
| 79 | + | ); | |
| 63 | 80 | ||
| 64 | 81 | const responseData: PublicRoomResponse = { | |
| 65 | 82 | success: true, | |
| @@ -24,42 +24,59 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 24 | // Calculate timeout threshold | 24 | // Calculate timeout threshold | ||
| 25 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | 25 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | ||
| 26 | 26 | ||||
| 27 | - | // Fetch buddy list: get all users and mark them as offline if they've timed out | 27 | + | // Fetch buddy list: get all users |
| 28 | const fetchedUsers = await db.select() | 28 | const fetchedUsers = await db.select() | ||
| 29 | .from(users) | 29 | .from(users) | ||
| 30 | .execute(); | 30 | .execute(); | ||
| 31 | 31 | ||||
| 32 | - | // Process users to mark them as offline if they've timed out | 32 | + | // Normalize users to ensure that lastSeen is always a number (using 0 if null) |
| 33 | - | const processedUsers = fetchedUsers.map((user: SafeUser & { lastSeen: number }) => { | 33 | + | const normalizedUsers = fetchedUsers.map( |
| 34 | - | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | 34 | + | (user: SafeUser & { lastSeen: number | null }) => ({ |
| 35 | - | return { | |||
| 36 | ...user, | 35 | ...user, | ||
| 37 | - | status: shouldBeOffline ? 'offline' : user.status | 36 | + | lastSeen: user.lastSeen ?? 0 |
| 38 | - | }; | 37 | + | }) |
| 39 | - | }); | 38 | + | ); |
| 40 | 39 | ||||
| 41 | - | // Update any users that should be offline in the database | 40 | + | // Process users to mark them as offline if they've timed out |
| 42 | - | const usersToUpdate = processedUsers.filter((user: SafeUser & { lastSeen: number }) => | 41 | + | const processedUsers = normalizedUsers.map( |
| 43 | - | user.status === 'offline' && | 42 | + | (user: SafeUser & { lastSeen: number }) => { |
| 44 | - | fetchedUsers.find((u: SafeUser & { lastSeen: number }) => u.id === user.id)?.status === 'online' | 43 | + | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; |
| 44 | + | return { | |||
| 45 | + | ...user, | |||
| 46 | + | status: shouldBeOffline ? 'offline' : user.status | |||
| 47 | + | }; | |||
| 48 | + | } | |||
| 49 | + | ); | |||
| 50 | + | ||||
| 51 | + | // Find users who were originally online but now marked offline | |||
| 52 | + | const usersToUpdate = processedUsers.filter( | |||
| 53 | + | (user: SafeUser & { lastSeen: number }) => | |||
| 54 | + | user.status === 'offline' && | |||
| 55 | + | normalizedUsers.find( | |||
| 56 | + | (u: SafeUser & { lastSeen: number }) => u.id === user.id | |||
| 57 | + | )?.status === 'online' | |||
| 45 | ); | 58 | ); | ||
| 46 | 59 | ||||
| 47 | if (usersToUpdate.length > 0) { | 60 | if (usersToUpdate.length > 0) { | ||
| 48 | - | await Promise.all(usersToUpdate.map((user: SafeUser & { lastSeen: number }) => | 61 | + | await Promise.all( |
| 49 | - | db.update(users) | 62 | + | usersToUpdate.map((user: SafeUser & { lastSeen: number }) => |
| 50 | - | .set({ status: 'offline', lastSeen: Date.now() }) | 63 | + | db.update(users) |
| 51 | - | .where(eq(users.id, user.id)) | 64 | + | .set({ status: 'offline', lastSeen: Date.now() }) |
| 52 | - | .execute() | 65 | + | .where(eq(users.id, user.id)) |
| 53 | - | )); | 66 | + | .execute() |
| 67 | + | ) | |||
| 68 | + | ); | |||
| 54 | 69 | ||||
| 55 | log.debug('Updated offline status for users', { | 70 | log.debug('Updated offline status for users', { | ||
| 56 | count: usersToUpdate.length, | 71 | count: usersToUpdate.length, | ||
| 57 | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) | 72 | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) | ||
| 58 | }); | 73 | }); | ||
| 59 | } | 74 | } | ||
| 60 | - | 75 | + | ||
| 61 | // Sanitize user data using createSafeUser | 76 | // Sanitize user data using createSafeUser | ||
| 62 | - | const sanitizedUsers = processedUsers.map((user: SafeUser & { lastSeen: number }) => createSafeUser(user)); | 77 | + | const sanitizedUsers = processedUsers.map( |
| 78 | + | (user: SafeUser & { lastSeen: number }) => createSafeUser(user) | |||
| 79 | + | ); | |||
| 63 | 80 | ||||
| 64 | const responseData: PublicRoomResponse = { | 81 | const responseData: PublicRoomResponse = { | ||
| 65 | success: true, | 82 | success: true, | ||
M src/routes/api/session/login/+server.ts +2 -1
| @@ -28,10 +28,11 @@ setInterval(() => { | |||
| 28 | 28 | } | |
| 29 | 29 | }, 60 * 60 * 1000); | |
| 30 | 30 | ||
| 31 | + | // cf_clearance:"Qwq0fwt23w" | |
| 31 | 32 | // Helper to parse clearance cookie from headers | |
| 32 | 33 | function getClearanceCookie(cookieHeader: string | null): string | null { | |
| 33 | 34 | if (!cookieHeader) return null; | |
| 34 | - | const match = cookieHeader.match(/cf-turnstile-clearance=([^;]+)/); | |
| 35 | + | const match = cookieHeader.match(/cf-clearance=([^;]+)/); | |
| 35 | 36 | return match ? match[1] : null; | |
| 36 | 37 | } | |
| 37 | 38 | ||
| @@ -28,10 +28,11 @@ setInterval(() => { | |||||
| 28 | } | 28 | } | ||
| 29 | }, 60 * 60 * 1000); | 29 | }, 60 * 60 * 1000); | ||
| 30 | 30 | ||||
| 31 | + | // cf_clearance:"Qwq0fwt23w" | |||
| 31 | // Helper to parse clearance cookie from headers | 32 | // Helper to parse clearance cookie from headers | ||
| 32 | function getClearanceCookie(cookieHeader: string | null): string | null { | 33 | function getClearanceCookie(cookieHeader: string | null): string | null { | ||
| 33 | if (!cookieHeader) return null; | 34 | if (!cookieHeader) return null; | ||
| 34 | - | const match = cookieHeader.match(/cf-turnstile-clearance=([^;]+)/); | 35 | + | const match = cookieHeader.match(/cf-clearance=([^;]+)/); |
| 35 | return match ? match[1] : null; | 36 | return match ? match[1] : null; | ||
| 36 | } | 37 | } | ||
| 37 | 38 | ||||