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:
@@ -31,6 +31,7 @@ export async function GET(request: NextRequest) {
|
|||||||
sites (name), zones (name)
|
sites (name), zones (name)
|
||||||
`)
|
`)
|
||||||
.order('reported_at', { ascending: false })
|
.order('reported_at', { ascending: false })
|
||||||
|
.limit(10000)
|
||||||
|
|
||||||
const rows = incidents ?? []
|
const rows = incidents ?? []
|
||||||
|
|
||||||
@@ -58,7 +59,15 @@ export async function GET(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const csv = rowsToCsv(headers, csvRows)
|
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, {
|
return new NextResponse(csv, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export async function GET() {
|
|||||||
const { data: incidents, error } = await supabase
|
const { data: incidents, error } = await supabase
|
||||||
.from('incidents')
|
.from('incidents')
|
||||||
.select('id, status, incident_type, sites (name)')
|
.select('id, status, incident_type, sites (name)')
|
||||||
|
.limit(10000)
|
||||||
|
|
||||||
if (error) return NextResponse.json({ error: 'Failed to fetch stats' }, { status: 500 })
|
if (error) return NextResponse.json({ error: 'Failed to fetch stats' }, { status: 500 })
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export async function POST(request: NextRequest) {
|
|||||||
if (!body.key || !ALLOWED_KEYS.includes(body.key as SettingKey)) {
|
if (!body.key || !ALLOWED_KEYS.includes(body.key as SettingKey)) {
|
||||||
return NextResponse.json({ error: 'Invalid key' }, { status: 422 })
|
return NextResponse.json({ error: 'Invalid key' }, { status: 422 })
|
||||||
}
|
}
|
||||||
if (typeof body.value !== 'string') {
|
if (typeof body.value !== 'string' || body.value.trim() === '') {
|
||||||
return NextResponse.json({ error: 'value required' }, { status: 422 })
|
return NextResponse.json({ error: 'value must be a non-empty string' }, { status: 422 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabase.from('app_settings').upsert({
|
const { error } = await supabase.from('app_settings').upsert({
|
||||||
|
|||||||
Reference in New Issue
Block a user