@@ -186,3 +186,190 @@ describe('File Upload API Route', () => {
186186 expect ( response . headers . get ( 'Access-Control-Allow-Headers' ) ) . toBe ( 'Content-Type' )
187187 } )
188188} )
189+
190+ describe ( 'File Upload Security Tests' , ( ) => {
191+ beforeEach ( ( ) => {
192+ vi . resetModules ( )
193+ vi . clearAllMocks ( )
194+
195+ vi . doMock ( '@/lib/auth' , ( ) => ( {
196+ getSession : vi . fn ( ) . mockResolvedValue ( {
197+ user : { id : 'test-user-id' } ,
198+ } ) ,
199+ } ) )
200+
201+ vi . doMock ( '@/lib/uploads' , ( ) => ( {
202+ isUsingCloudStorage : vi . fn ( ) . mockReturnValue ( false ) ,
203+ uploadFile : vi . fn ( ) . mockResolvedValue ( {
204+ key : 'test-key' ,
205+ path : '/test/path' ,
206+ } ) ,
207+ } ) )
208+
209+ vi . doMock ( '@/lib/uploads/setup.server' , ( ) => ( { } ) )
210+ } )
211+
212+ afterEach ( ( ) => {
213+ vi . clearAllMocks ( )
214+ } )
215+
216+ describe ( 'File Extension Validation' , ( ) => {
217+ it ( 'should accept allowed file types' , async ( ) => {
218+ const allowedTypes = [
219+ 'pdf' ,
220+ 'doc' ,
221+ 'docx' ,
222+ 'txt' ,
223+ 'md' ,
224+ 'png' ,
225+ 'jpg' ,
226+ 'jpeg' ,
227+ 'gif' ,
228+ 'csv' ,
229+ 'xlsx' ,
230+ 'xls' ,
231+ ]
232+
233+ for ( const ext of allowedTypes ) {
234+ const formData = new FormData ( )
235+ const file = new File ( [ 'test content' ] , `test.${ ext } ` , { type : 'application/octet-stream' } )
236+ formData . append ( 'file' , file )
237+
238+ const req = new Request ( 'http://localhost/api/files/upload' , {
239+ method : 'POST' ,
240+ body : formData ,
241+ } )
242+
243+ const { POST } = await import ( '@/app/api/files/upload/route' )
244+ const response = await POST ( req as any )
245+
246+ expect ( response . status ) . toBe ( 200 )
247+ }
248+ } )
249+
250+ it ( 'should reject HTML files to prevent XSS' , async ( ) => {
251+ const formData = new FormData ( )
252+ const maliciousContent = '<script>alert("XSS")</script>'
253+ const file = new File ( [ maliciousContent ] , 'malicious.html' , { type : 'text/html' } )
254+ formData . append ( 'file' , file )
255+
256+ const req = new Request ( 'http://localhost/api/files/upload' , {
257+ method : 'POST' ,
258+ body : formData ,
259+ } )
260+
261+ const { POST } = await import ( '@/app/api/files/upload/route' )
262+ const response = await POST ( req as any )
263+
264+ expect ( response . status ) . toBe ( 400 )
265+ const data = await response . json ( )
266+ expect ( data . message ) . toContain ( "File type 'html' is not allowed" )
267+ } )
268+
269+ it ( 'should reject SVG files to prevent XSS' , async ( ) => {
270+ const formData = new FormData ( )
271+ const maliciousSvg = '<svg onload="alert(\'XSS\')" xmlns="http://www.w3.org/2000/svg"></svg>'
272+ const file = new File ( [ maliciousSvg ] , 'malicious.svg' , { type : 'image/svg+xml' } )
273+ formData . append ( 'file' , file )
274+
275+ const req = new Request ( 'http://localhost/api/files/upload' , {
276+ method : 'POST' ,
277+ body : formData ,
278+ } )
279+
280+ const { POST } = await import ( '@/app/api/files/upload/route' )
281+ const response = await POST ( req as any )
282+
283+ expect ( response . status ) . toBe ( 400 )
284+ const data = await response . json ( )
285+ expect ( data . message ) . toContain ( "File type 'svg' is not allowed" )
286+ } )
287+
288+ it ( 'should reject JavaScript files' , async ( ) => {
289+ const formData = new FormData ( )
290+ const maliciousJs = 'alert("XSS")'
291+ const file = new File ( [ maliciousJs ] , 'malicious.js' , { type : 'application/javascript' } )
292+ formData . append ( 'file' , file )
293+
294+ const req = new Request ( 'http://localhost/api/files/upload' , {
295+ method : 'POST' ,
296+ body : formData ,
297+ } )
298+
299+ const { POST } = await import ( '@/app/api/files/upload/route' )
300+ const response = await POST ( req as any )
301+
302+ expect ( response . status ) . toBe ( 400 )
303+ const data = await response . json ( )
304+ expect ( data . message ) . toContain ( "File type 'js' is not allowed" )
305+ } )
306+
307+ it ( 'should reject files without extensions' , async ( ) => {
308+ const formData = new FormData ( )
309+ const file = new File ( [ 'test content' ] , 'noextension' , { type : 'application/octet-stream' } )
310+ formData . append ( 'file' , file )
311+
312+ const req = new Request ( 'http://localhost/api/files/upload' , {
313+ method : 'POST' ,
314+ body : formData ,
315+ } )
316+
317+ const { POST } = await import ( '@/app/api/files/upload/route' )
318+ const response = await POST ( req as any )
319+
320+ expect ( response . status ) . toBe ( 400 )
321+ const data = await response . json ( )
322+ expect ( data . message ) . toContain ( "File type 'noextension' is not allowed" )
323+ } )
324+
325+ it ( 'should handle multiple files with mixed valid/invalid types' , async ( ) => {
326+ const formData = new FormData ( )
327+
328+ // Valid file
329+ const validFile = new File ( [ 'valid content' ] , 'valid.pdf' , { type : 'application/pdf' } )
330+ formData . append ( 'file' , validFile )
331+
332+ // Invalid file (should cause rejection of entire request)
333+ const invalidFile = new File ( [ '<script>alert("XSS")</script>' ] , 'malicious.html' , {
334+ type : 'text/html' ,
335+ } )
336+ formData . append ( 'file' , invalidFile )
337+
338+ const req = new Request ( 'http://localhost/api/files/upload' , {
339+ method : 'POST' ,
340+ body : formData ,
341+ } )
342+
343+ const { POST } = await import ( '@/app/api/files/upload/route' )
344+ const response = await POST ( req as any )
345+
346+ expect ( response . status ) . toBe ( 400 )
347+ const data = await response . json ( )
348+ expect ( data . message ) . toContain ( "File type 'html' is not allowed" )
349+ } )
350+ } )
351+
352+ describe ( 'Authentication Requirements' , ( ) => {
353+ it ( 'should reject uploads without authentication' , async ( ) => {
354+ vi . doMock ( '@/lib/auth' , ( ) => ( {
355+ getSession : vi . fn ( ) . mockResolvedValue ( null ) ,
356+ } ) )
357+
358+ const formData = new FormData ( )
359+ const file = new File ( [ 'test content' ] , 'test.pdf' , { type : 'application/pdf' } )
360+ formData . append ( 'file' , file )
361+
362+ const req = new Request ( 'http://localhost/api/files/upload' , {
363+ method : 'POST' ,
364+ body : formData ,
365+ } )
366+
367+ const { POST } = await import ( '@/app/api/files/upload/route' )
368+ const response = await POST ( req as any )
369+
370+ expect ( response . status ) . toBe ( 401 )
371+ const data = await response . json ( )
372+ expect ( data . error ) . toBe ( 'Unauthorized' )
373+ } )
374+ } )
375+ } )
0 commit comments