security: add CSV injection protection to all export routes

Add sanitizeCsvCell() helper to both export routes that prefixes cell
values starting with =, +, @, or - with a single quote to prevent CSV
formula injection attacks.

Applied to:
- src/app/api/export/route.ts: timestamp and label_type columns
- src/app/api/span-annotations/export/route.ts: start_time, end_time,
  label, and outcome columns

Closes task 4.10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-18 11:20:36 +01:00
parent 160f146ab4
commit b2129ad626
3 changed files with 18 additions and 4 deletions

View file

@ -3,6 +3,13 @@ import { db } from '@/lib/db';
import { annotations, candles, charts } from '@/lib/db/schema';
import { eq, and, desc } from 'drizzle-orm';
function sanitizeCsvCell(value: string): string {
if (/^[=+@\-]/.test(value)) {
return `'${value}`;
}
return value;
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = request.nextUrl;
@ -57,7 +64,7 @@ export async function GET(request: NextRequest) {
}
csvRows.push(
`${annotation.timestamp},${annotation.label_type},${price !== null ? price : ''}`
`${sanitizeCsvCell(String(annotation.timestamp))},${sanitizeCsvCell(annotation.label_type)},${price !== null ? price : ''}`
);
}

View file

@ -3,6 +3,13 @@ import { db } from '@/lib/db';
import { spanAnnotations, charts } from '@/lib/db/schema';
import { eq, desc } from 'drizzle-orm';
function sanitizeCsvCell(value: string): string {
if (/^[=+@\-]/.test(value)) {
return `'${value}`;
}
return value;
}
/**
* GET /api/span-annotations/export
*
@ -97,9 +104,9 @@ export async function GET(request: NextRequest) {
for (const span of spans) {
const notes = span.notes ? `"${span.notes.replace(/"/g, '""')}"` : '';
csvRows.push(
`${span.id},${span.start_time},${span.end_time},${span.label},${span.confidence || ''},${span.outcome || ''},${notes}`
`${span.id},${sanitizeCsvCell(String(span.start_time))},${sanitizeCsvCell(String(span.end_time))},${sanitizeCsvCell(span.label)},${span.confidence || ''},${span.outcome ? sanitizeCsvCell(span.outcome) : ''},${notes}`
);
}