@@ -2832,6 +2832,170 @@ describe('Vulnerabilities', () => {
28322832 } ) ;
28332833 } ) ;
28342834
2835+ describe ( '(GHSA-7wqv-xjf3-x35v) Stored XSS via trailing-dot filename bypassing file extension blocklist' , ( ) => {
2836+ const headers = {
2837+ 'X-Parse-Application-Id' : 'test' ,
2838+ 'X-Parse-REST-API-Key' : 'rest' ,
2839+ } ;
2840+
2841+ beforeEach ( async ( ) => {
2842+ await reconfigureServer ( {
2843+ fileUpload : {
2844+ enableForPublic : true ,
2845+ } ,
2846+ } ) ;
2847+ } ) ;
2848+
2849+ it ( 'blocks trailing-dot SVG filename with dangerous _ContentType on JSON-body upload' , async ( ) => {
2850+ const svgContent = Buffer . from (
2851+ '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>'
2852+ ) . toString ( 'base64' ) ;
2853+ await expectAsync (
2854+ request ( {
2855+ method : 'POST' ,
2856+ url : 'http://localhost:8378/1/files/poc.svg.' ,
2857+ body : JSON . stringify ( {
2858+ _ApplicationId : 'test' ,
2859+ _JavaScriptKey : 'test' ,
2860+ _ContentType : 'image/svg+xml' ,
2861+ base64 : svgContent ,
2862+ } ) ,
2863+ } ) . catch ( e => {
2864+ throw new Error ( e . data . error ) ;
2865+ } )
2866+ ) . toBeRejectedWith ( jasmine . objectContaining ( {
2867+ message : jasmine . stringMatching ( / F i l e u p l o a d o f e x t e n s i o n .+ i s d i s a b l e d / ) ,
2868+ } ) ) ;
2869+ } ) ;
2870+
2871+ it ( 'blocks trailing-dot SVG filename with dangerous Content-Type on binary upload' , async ( ) => {
2872+ await expectAsync (
2873+ request ( {
2874+ method : 'POST' ,
2875+ headers : {
2876+ ...headers ,
2877+ 'Content-Type' : 'image/svg+xml' ,
2878+ } ,
2879+ url : 'http://localhost:8378/1/files/poc.svg.' ,
2880+ body : '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>' ,
2881+ } ) . catch ( e => {
2882+ throw new Error ( e . data . error ) ;
2883+ } )
2884+ ) . toBeRejectedWith ( jasmine . objectContaining ( {
2885+ message : jasmine . stringMatching ( / F i l e u p l o a d o f e x t e n s i o n .+ i s d i s a b l e d / ) ,
2886+ } ) ) ;
2887+ } ) ;
2888+
2889+ it ( 'blocks filename with mixed trailing dots and whitespace' , async ( ) => {
2890+ for ( const filename of [ 'poc.svg..' , 'poc.svg. ' , 'poc.svg . ' ] ) {
2891+ await expectAsync (
2892+ request ( {
2893+ method : 'POST' ,
2894+ headers : {
2895+ ...headers ,
2896+ 'Content-Type' : 'image/svg+xml' ,
2897+ } ,
2898+ url : `http://localhost:8378/1/files/${ encodeURIComponent ( filename ) } ` ,
2899+ body : '<svg/>' ,
2900+ } ) . catch ( e => {
2901+ throw new Error ( e . data . error ) ;
2902+ } )
2903+ ) . toBeRejectedWith ( jasmine . objectContaining ( {
2904+ message : jasmine . stringMatching ( / F i l e u p l o a d o f e x t e n s i o n .+ i s d i s a b l e d / ) ,
2905+ } ) ) ;
2906+ }
2907+ } ) ;
2908+
2909+ it ( 'still allows trailing-dot filename with allowed Content-Type' , async ( ) => {
2910+ const adapter = Config . get ( 'test' ) . filesController . adapter ;
2911+ const spy = spyOn ( adapter , 'createFile' ) . and . callThrough ( ) ;
2912+ const response = await request ( {
2913+ method : 'POST' ,
2914+ url : 'http://localhost:8378/1/files/notes.txt.' ,
2915+ body : JSON . stringify ( {
2916+ _ApplicationId : 'test' ,
2917+ _JavaScriptKey : 'test' ,
2918+ _ContentType : 'text/plain' ,
2919+ base64 : Buffer . from ( 'hello' ) . toString ( 'base64' ) ,
2920+ } ) ,
2921+ headers,
2922+ } ) ;
2923+ expect ( response . status ) . toBe ( 201 ) ;
2924+ expect ( spy ) . toHaveBeenCalled ( ) ;
2925+ } ) ;
2926+
2927+ it ( 'FilesController treats trailing-dot filename as extensionless when appending derived extension via master key upload' , async ( ) => {
2928+ await reconfigureServer ( {
2929+ fileUpload : {
2930+ enableForPublic : true ,
2931+ } ,
2932+ preserveFileName : true ,
2933+ } ) ;
2934+ const adapter = Config . get ( 'test' ) . filesController . adapter ;
2935+ const spy = spyOn ( adapter , 'createFile' ) . and . callThrough ( ) ;
2936+ const response = await request ( {
2937+ method : 'POST' ,
2938+ url : 'http://localhost:8378/1/files/poc.svg.' ,
2939+ headers : {
2940+ 'X-Parse-Application-Id' : 'test' ,
2941+ 'X-Parse-Master-Key' : 'test' ,
2942+ 'Content-Type' : 'image/svg+xml' ,
2943+ } ,
2944+ body : '<svg/>' ,
2945+ } ) ;
2946+ expect ( response . status ) . toBe ( 201 ) ;
2947+ expect ( spy ) . toHaveBeenCalled ( ) ;
2948+ const filenameArg = spy . calls . mostRecent ( ) . args [ 0 ] ;
2949+ const contentTypeArg = spy . calls . mostRecent ( ) . args [ 2 ] ;
2950+ expect ( filenameArg ) . toBe ( 'poc.svg.svg' ) ;
2951+ expect ( contentTypeArg ) . toBe ( 'image/svg+xml' ) ;
2952+ } ) ;
2953+
2954+ it ( 'allows trailing-dot filename when no Content-Type is supplied (no XSS path)' , async ( ) => {
2955+ // Trailing-dot filename with no caller-supplied Content-Type: the
2956+ // blocklist gate skips because no extension can be determined, but no
2957+ // attacker-controlled Content-Type reaches the storage adapter — only
2958+ // the SDK's benign default — so no stored XSS is possible.
2959+ const adapter = Config . get ( 'test' ) . filesController . adapter ;
2960+ const spy = spyOn ( adapter , 'createFile' ) . and . callThrough ( ) ;
2961+ const response = await request ( {
2962+ method : 'POST' ,
2963+ headers : {
2964+ 'X-Parse-Application-Id' : 'test' ,
2965+ 'X-Parse-REST-API-Key' : 'rest' ,
2966+ } ,
2967+ url : 'http://localhost:8378/1/files/poc.svg.' ,
2968+ body : '<svg/>' ,
2969+ } ) ;
2970+ expect ( response . status ) . toBe ( 201 ) ;
2971+ expect ( spy ) . toHaveBeenCalled ( ) ;
2972+ const contentTypeArg = spy . calls . mostRecent ( ) . args [ 2 ] ;
2973+ expect ( contentTypeArg ) . not . toMatch ( / s v g | h t m l | x m l | x h t m l | x s l t | m a t h m l / i) ;
2974+ } ) ;
2975+
2976+ it ( 'falls back to raw Content-Type when Content-Type is malformed (no slash)' , async ( ) => {
2977+ // Exercises the last-resort branch: when both the filename has no usable
2978+ // extension AND the Content-Type lacks a "/" subtype to parse, the raw
2979+ // Content-Type is used as the extension so a malformed header that
2980+ // matches a blocked pattern still trips the blocklist.
2981+ await expectAsync (
2982+ request ( {
2983+ method : 'POST' ,
2984+ headers : {
2985+ ...headers ,
2986+ 'Content-Type' : 'svg' ,
2987+ } ,
2988+ url : 'http://localhost:8378/1/files/poc' ,
2989+ body : '<svg/>' ,
2990+ } ) . catch ( e => {
2991+ throw new Error ( e . data . error ) ;
2992+ } )
2993+ ) . toBeRejectedWith ( jasmine . objectContaining ( {
2994+ message : jasmine . stringMatching ( / F i l e u p l o a d o f e x t e n s i o n s v g i s d i s a b l e d / ) ,
2995+ } ) ) ;
2996+ } ) ;
2997+ } ) ;
2998+
28352999 describe ( '(GHSA-9ccr-fpp6-78qf) Schema poisoning via __proto__ bypassing requestKeywordDenylist and addField CLP' , ( ) => {
28363000 const headers = {
28373001 'Content-Type' : 'application/json' ,
0 commit comments