diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..2fb954e --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-05-23 - [Critical Security Learning - Sentinel Init] +**Vulnerability:** Initial scan revealed minor issues but solid foundation. +**Learning:** Project uses Supabase RLS and `withSecurity` middleware. +**Prevention:** Maintain patterns. diff --git a/app/api/notes/route.ts b/app/api/notes/route.ts index 739ec63..fdf8a6c 100644 --- a/app/api/notes/route.ts +++ b/app/api/notes/route.ts @@ -1,11 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { withSecurity, SECURITY_PRESETS } from '@/lib/security-middleware'; -import { formatValidationError, noteDeleteSchema, noteInsertSchema } from '@/lib/validation'; +import { formatValidationError, noteDeleteSchema, noteInsertSchema, youtubeIdSchema } from '@/lib/validation'; import { z } from 'zod'; const getNotesQuerySchema = z.object({ - youtubeId: z.string() + youtubeId: youtubeIdSchema }); interface NoteRow { diff --git a/app/api/transcript/route.ts b/app/api/transcript/route.ts index a997253..ebd54a0 100644 --- a/app/api/transcript/route.ts +++ b/app/api/transcript/route.ts @@ -4,6 +4,15 @@ import { withSecurity, SECURITY_PRESETS } from '@/lib/security-middleware'; import { shouldUseMockData, getMockTranscript } from '@/lib/mock-data'; import { mergeTranscriptSegmentsIntoSentences } from '@/lib/transcript-sentence-merger'; import { NO_CREDITS_USED_MESSAGE } from '@/lib/no-credits-message'; +import { z } from 'zod'; + +// Strict validation for language code (ISO 639-1) +// Only allow 2-10 alphanumeric characters and dashes +const langSchema = z.string() + .min(2) + .max(10) + .regex(/^[a-zA-Z0-9-]+$/) + .optional(); function respondWithNoCredits( payload: Record, @@ -33,6 +42,13 @@ async function handler(request: NextRequest) { return respondWithNoCredits({ error: 'Invalid YouTube URL' }, 400); } + // Validate lang if provided + try { + langSchema.parse(lang); + } catch { + return respondWithNoCredits({ error: 'Invalid language code format' }, 400); + } + if (shouldUseMockData()) { console.log( '[TRANSCRIPT] Using mock data (NEXT_PUBLIC_USE_MOCK_DATA=true)' @@ -210,7 +226,7 @@ async function handler(request: NextRequest) { } const rawSegments = Array.isArray(transcriptSegments) - ? transcriptSegments.map((item, idx) => { + ? transcriptSegments.map((item) => { const transformed = { text: item.text || item.content || '', // Convert milliseconds to seconds for offset/start