From 8fe036bc1acc13e3672aabd1e1284e696f32023c Mon Sep 17 00:00:00 2001 From: weeihan Date: Sun, 12 Jul 2026 21:25:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20P2=20API=20hardening=20=E2=80=94=20unbou?= =?UTF-8?q?nded=20SELECTs,=20export=20audit=20log,=20header=20injection,?= =?UTF-8?q?=20empty-key=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - export: add .limit(10000), sanitize filename, write_audit_log on every export - stats: add .limit(10000) to aggregation query - settings POST: reject empty string values to prevent silent key deletion Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ --- app/api/dashboard/export/route.ts | 11 ++++++++++- app/api/dashboard/stats/route.ts | 1 + app/api/settings/route.ts | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/api/dashboard/export/route.ts b/app/api/dashboard/export/route.ts index 5558420..49f2d9e 100644 --- a/app/api/dashboard/export/route.ts +++ b/app/api/dashboard/export/route.ts @@ -31,6 +31,7 @@ export async function GET(request: NextRequest) { sites (name), zones (name) `) .order('reported_at', { ascending: false }) + .limit(10000) const rows = incidents ?? [] @@ -58,7 +59,15 @@ export async function GET(request: NextRequest) { }) const csv = rowsToCsv(headers, csvRows) - const filename = `incidents-${role}-${new Date().toISOString().split('T')[0]}.csv` + const safeRole = role.replace(/[^a-z0-9]/gi, '') + const filename = `incidents-${safeRole}-${new Date().toISOString().split('T')[0]}.csv` + + await supabase.rpc('write_audit_log', { + p_table_name: 'incidents', + p_record_id: user.id, + p_action: 'export_csv', + p_new_value: { role, row_count: rows.length } as never, + }) return new NextResponse(csv, { status: 200, diff --git a/app/api/dashboard/stats/route.ts b/app/api/dashboard/stats/route.ts index 67e1843..0c03377 100644 --- a/app/api/dashboard/stats/route.ts +++ b/app/api/dashboard/stats/route.ts @@ -15,6 +15,7 @@ export async function GET() { const { data: incidents, error } = await supabase .from('incidents') .select('id, status, incident_type, sites (name)') + .limit(10000) if (error) return NextResponse.json({ error: 'Failed to fetch stats' }, { status: 500 }) diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 08f1aaa..7d25500 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -47,8 +47,8 @@ export async function POST(request: NextRequest) { if (!body.key || !ALLOWED_KEYS.includes(body.key as SettingKey)) { return NextResponse.json({ error: 'Invalid key' }, { status: 422 }) } - if (typeof body.value !== 'string') { - return NextResponse.json({ error: 'value required' }, { status: 422 }) + if (typeof body.value !== 'string' || body.value.trim() === '') { + return NextResponse.json({ error: 'value must be a non-empty string' }, { status: 422 }) } const { error } = await supabase.from('app_settings').upsert({