fix: P2 API hardening — unbounded SELECTs, export audit log, header injection, empty-key guard

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
This commit is contained in:
2026-07-12 21:25:35 +08:00
co-authored by Claude Sonnet 4.6
parent 16dd62df11
commit 8fe036bc1a
3 changed files with 13 additions and 3 deletions
+10 -1
View File
@@ -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,
+1
View File
@@ -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 })
+2 -2
View File
@@ -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({