@@ -139,16 +139,26 @@ function getAudioCodec(format: string): string {
139139}
140140
141141/**
142- * Derives a sensible file extension for an input temp file from its name or MIME type.
142+ * Reduces a user- or filename-derived extension to a safe `[a-z0-9]` token.
143+ * Strips path separators, dots, and other metacharacters so the value can be
144+ * interpolated into a temp file name without enabling path traversal.
143145 */
144- function getInputExtension ( file : UserFile ) : string {
145- const fromName = path
146- . extname ( file . name || '' )
147- . replace ( '.' , '' )
146+ function safeExtension ( value : string | undefined ) : string {
147+ return ( value ?? '' )
148+ . trim ( )
148149 . toLowerCase ( )
150+ . replace ( / [ ^ a - z 0 - 9 ] / g, '' )
151+ . slice ( 0 , 8 )
152+ }
153+
154+ /**
155+ * Derives a safe file extension for an input temp file from its name or MIME type.
156+ */
157+ function getInputExtension ( file : UserFile ) : string {
158+ const fromName = safeExtension ( path . extname ( file . name || '' ) )
149159 if ( fromName ) return fromName
150- const subtype = ( file . type || '' ) . split ( '/' ) [ 1 ]
151- return subtype ? subtype . toLowerCase ( ) : 'dat'
160+ const subtype = safeExtension ( ( file . type || '' ) . split ( '/' ) [ 1 ] )
161+ return subtype || 'dat'
152162}
153163
154164function isVideoExtension ( ext : string ) : boolean {
@@ -402,13 +412,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
402412 let mimeType = getMimeForFormat ( inputExt )
403413
404414 if ( operation === 'convert' ) {
405- if ( ! body . format ) {
415+ outExt = safeExtension ( body . format )
416+ if ( ! outExt ) {
406417 return NextResponse . json (
407- { error : 'format is required for the convert operation' } ,
418+ { error : 'A valid output format is required for the convert operation (e.g. mp4, mp3) ' } ,
408419 { status : 400 }
409420 )
410421 }
411- outExt = body . format . trim ( ) . toLowerCase ( )
412422 mimeType = getMimeForFormat ( outExt )
413423 const outputPath = path . join ( tempDir , `output.${ outExt } ` )
414424 await runFfmpeg ( ( cmd ) => {
@@ -421,7 +431,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
421431 }
422432
423433 if ( operation === 'extract_audio' ) {
424- outExt = ( body . format ?. trim ( ) || 'mp3' ) . toLowerCase ( )
434+ outExt = safeExtension ( body . format ) || 'mp3'
425435 mimeType = getMimeForFormat ( outExt )
426436 const outputPath = path . join ( tempDir , `output.${ outExt } ` )
427437 await runFfmpeg ( ( cmd ) =>
@@ -478,7 +488,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
478488 }
479489
480490 if ( operation === 'thumbnail' ) {
481- outExt = ( body . format ?. trim ( ) || 'jpg' ) . toLowerCase ( )
491+ outExt = safeExtension ( body . format ) || 'jpg'
482492 mimeType = getMimeForFormat ( outExt )
483493 const time = body . time || '00:00:01'
484494 const outputPath = path . join ( tempDir , `output.${ outExt } ` )
@@ -553,13 +563,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
553563 context : ExecutionContext | null ,
554564 userId ?: string
555565 ) : Promise < NextResponse > {
556- const outputBuffer = await fs . readFile ( outputPath )
557- if ( outputBuffer . length === 0 ) {
566+ // Check size via stat before reading so an oversized output is rejected
567+ // without first pulling the entire file into memory.
568+ const { size : outputSize } = await fs . stat ( outputPath )
569+ if ( outputSize === 0 ) {
558570 throw new Error ( 'FFmpeg produced an empty output file' )
559571 }
560- if ( outputBuffer . length > MAX_FFMPEG_OUTPUT_BYTES ) {
572+ if ( outputSize > MAX_FFMPEG_OUTPUT_BYTES ) {
561573 throw new Error ( 'Output file exceeds the maximum allowed size' )
562574 }
575+ const outputBuffer = await fs . readFile ( outputPath )
563576 const fileName = `ffmpeg-${ operation } -${ Date . now ( ) } .${ format } `
564577 const file = await storeOutputFile ( outputBuffer , fileName , mimeType , context , userId )
565578 logger . info ( `[${ requestId } ] FFmpeg ${ operation } completed` , {
0 commit comments