fix(#55): implement user data isolation to prevent cross-user data access

- Add server-side authentication validation for all protected API endpoints
- Create auth utility with requireAuth() function for session validation
- Update CombinedDataRepository to filter data by authenticated user ID
- Add findByUserId() method to CatchRecordRepository for user-specific queries
- Replace client-side userId parameters with server-side session extraction
- Use MongoDB aggregation with $lookup and $expr for secure user filtering
- Return 401 errors for unauthenticated requests
- Fix critical security vulnerability where users could see others' catch records

Fixes: User data isolation bug where one user's catch records were used for everyone
Security: Prevents unauthorized access to other users' Pokemon tracking data
This commit is contained in:
Josh Creek
2025-07-26 16:34:19 +01:00
parent a4269259f9
commit 3aa716b941
7 changed files with 172 additions and 39 deletions
+31 -9
View File
@@ -1,19 +1,27 @@
import { json } from '@sveltejs/kit';
import { json, error } from '@sveltejs/kit';
import { dbConnect, dbDisconnect } from '$lib/utils/db';
import { type CatchRecord } from '$lib/models/CatchRecord';
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
export const GET = async () => {
export const GET = async (event: RequestEvent) => {
let catchData = null as CatchRecord[] | null;
try {
const userId = await requireAuth(event);
await dbConnect();
const repo = new CatchRecordRepository();
catchData = await repo.findAll();
catchData = await repo.findByUserId(userId);
// order by pokedexEntryId property, ascending
catchData = catchData.sort((a, b) => a.pokedexEntryId - b.pokedexEntryId);
} catch (error) {
console.error(error);
if (error.status) {
// Re-throw SvelteKit errors (like 401)
throw error;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
} finally {
dbDisconnect();
}
@@ -21,29 +29,40 @@ export const GET = async () => {
return json(catchData);
};
export const PUT = async ({ request }) => {
export const PUT = async (event: RequestEvent) => {
try {
const data: Partial<CatchRecord> = await request.json();
const userId = await requireAuth(event);
const data: Partial<CatchRecord> = await event.request.json();
// Ensure the userId is set to the authenticated user
data.userId = userId;
await dbConnect();
const repo = new CatchRecordRepository();
const upsertedRecord = await repo.upsert(data);
return json(upsertedRecord);
} catch (err) {
console.error(err);
throw error(500, 'Internal Server Error');
if (err.status) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
} finally {
await dbDisconnect();
}
};
export const POST = async ({ request }) => {
export const POST = async (event: RequestEvent) => {
try {
const records: Partial<CatchRecord>[] = await request.json();
const userId = await requireAuth(event);
const records: Partial<CatchRecord>[] = await event.request.json();
await dbConnect();
const repo = new CatchRecordRepository();
const insertedRecords = [];
for (const record of records) {
// Ensure the userId is set to the authenticated user
record.userId = userId;
const upsertedRecord = await repo.upsert(record);
insertedRecords.push(upsertedRecord);
}
@@ -51,7 +70,10 @@ export const POST = async ({ request }) => {
return json(insertedRecords);
} catch (err) {
console.error(err);
throw error(500, 'Internal Server Error');
if (err.status) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
} finally {
await dbDisconnect();
}
+13 -4
View File
@@ -1,8 +1,11 @@
import { json } from '@sveltejs/kit';
import { dbConnect, dbDisconnect } from '$lib/utils/db';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
export const GET = async ({ url }) => {
export const GET = async (event: RequestEvent) => {
const { url } = event;
const page = parseInt(url.searchParams.get('page') || '1', 10);
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const enableForms = url.searchParams.get('enableForms') === 'true';
@@ -10,19 +13,25 @@ export const GET = async ({ url }) => {
const game = url.searchParams.get('game');
try {
const userId = await requireAuth(event);
await dbConnect();
const repo = new CombinedDataRepository();
const combinedData = await repo.findCombinedData(page, limit, enableForms, region, game);
const combinedData = await repo.findCombinedData(userId, page, limit, enableForms, region, game);
// Return empty array instead of 404 for better UX
if (combinedData.length === 0) {
return json({ error: 'No combined data found' }, { status: 404 });
return json({ combinedData: [], totalPages: 0 });
}
const totalCount = await repo.countCombinedData(enableForms, region, game);
const totalCount = await repo.countCombinedData(userId, enableForms, region, game);
const totalPages = Math.ceil(totalCount / limit);
return json({ combinedData, totalPages });
} catch (error) {
console.error(error);
if (error.status) {
throw error;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
} finally {
dbDisconnect();
+10 -5
View File
@@ -1,23 +1,28 @@
import { json } from '@sveltejs/kit';
import { dbConnect, dbDisconnect } from '$lib/utils/db';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
export const GET = async ({ url }) => {
export const GET = async (event: RequestEvent) => {
const { url } = event;
const enableForms = url.searchParams.get('enableForms') === 'true';
const region = url.searchParams.get('region');
const game = url.searchParams.get('game');
try {
const userId = await requireAuth(event);
await dbConnect();
const repo = new CombinedDataRepository();
const combinedData = await repo.findAllCombinedData(enableForms, region, game);
const combinedData = await repo.findAllCombinedData(userId, enableForms, region, game);
if (combinedData.length === 0) {
return json({ error: 'No combined data found' }, { status: 404 });
}
// Return empty array instead of 404 for better UX
return json(combinedData);
} catch (error) {
console.error(error);
if (error.status) {
throw error;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
} finally {
dbDisconnect();