@@ -375,6 +375,113 @@ describe('routeAllowList', () => {
375375 } ) ;
376376 } ) ;
377377
378+ describe ( 'batch sub-requests' , ( ) => {
379+ // routeAllowList must be enforced per batch sub-request. The outer
380+ // enforceRouteAllowList middleware runs only on the outer /batch URL,
381+ // so without per-sub-request enforcement an operator who allowlists
382+ // `batch` would accidentally expose every REST route reachable through
383+ // batch sub-request dispatch.
384+ const restRequest = require ( '../lib/request' ) ;
385+ const headers = {
386+ 'Content-Type' : 'application/json' ,
387+ 'X-Parse-Application-Id' : 'test' ,
388+ 'X-Parse-REST-API-Key' : 'rest' ,
389+ } ;
390+
391+ it ( 'blocks a batch GET sub-request whose path is not allowlisted' , async ( ) => {
392+ await reconfigureServer ( { routeAllowList : [ 'batch' ] } ) ;
393+ await new Parse . Object ( 'Blocked' ) . save ( { secret : 'x' } , { useMasterKey : true } ) ;
394+ try {
395+ await restRequest ( {
396+ method : 'POST' ,
397+ headers,
398+ url : 'http://localhost:8378/1/batch' ,
399+ body : JSON . stringify ( {
400+ requests : [ { method : 'GET' , path : '/1/classes/Blocked' } ] ,
401+ } ) ,
402+ } ) ;
403+ fail ( 'batch sub-request to a blocked route should have been rejected' ) ;
404+ } catch ( e ) {
405+ expect ( e . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
406+ }
407+ } ) ;
408+
409+ it ( 'blocks a batch POST sub-request whose path is not allowlisted' , async ( ) => {
410+ await reconfigureServer ( { routeAllowList : [ 'batch' ] } ) ;
411+ try {
412+ await restRequest ( {
413+ method : 'POST' ,
414+ headers,
415+ url : 'http://localhost:8378/1/batch' ,
416+ body : JSON . stringify ( {
417+ requests : [ { method : 'POST' , path : '/1/classes/Blocked' , body : { x : 1 } } ] ,
418+ } ) ,
419+ } ) ;
420+ fail ( 'batch sub-request POST to a blocked route should have been rejected' ) ;
421+ } catch ( e ) {
422+ expect ( e . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
423+ }
424+ const query = new Parse . Query ( 'Blocked' ) ;
425+ const results = await query . find ( { useMasterKey : true } ) ;
426+ expect ( results . length ) . toBe ( 0 ) ;
427+ } ) ;
428+
429+ it ( 'allows a batch sub-request whose path matches the allow list' , async ( ) => {
430+ await reconfigureServer ( { routeAllowList : [ 'batch' , 'classes/Allowed' ] } ) ;
431+ const response = await restRequest ( {
432+ method : 'POST' ,
433+ headers,
434+ url : 'http://localhost:8378/1/batch' ,
435+ body : JSON . stringify ( {
436+ requests : [ { method : 'POST' , path : '/1/classes/Allowed' , body : { x : 1 } } ] ,
437+ } ) ,
438+ } ) ;
439+ expect ( response . data . length ) . toBe ( 1 ) ;
440+ expect ( response . data [ 0 ] . success . objectId ) . toBeDefined ( ) ;
441+ } ) ;
442+
443+ it ( 'rejects the entire batch if any sub-request is not allowlisted' , async ( ) => {
444+ await reconfigureServer ( { routeAllowList : [ 'batch' , 'classes/Allowed' ] } ) ;
445+ try {
446+ await restRequest ( {
447+ method : 'POST' ,
448+ headers,
449+ url : 'http://localhost:8378/1/batch' ,
450+ body : JSON . stringify ( {
451+ requests : [
452+ { method : 'POST' , path : '/1/classes/Allowed' , body : { x : 1 } } ,
453+ { method : 'POST' , path : '/1/classes/Blocked' , body : { y : 2 } } ,
454+ ] ,
455+ } ) ,
456+ } ) ;
457+ fail ( 'batch with any disallowed sub-request should have been rejected' ) ;
458+ } catch ( e ) {
459+ expect ( e . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
460+ }
461+ const allowedQuery = new Parse . Query ( 'Allowed' ) ;
462+ const allowedResults = await allowedQuery . find ( { useMasterKey : true } ) ;
463+ expect ( allowedResults . length ) . toBe ( 0 ) ;
464+ } ) ;
465+
466+ it ( 'allows master key to bypass sub-request allow-list check' , async ( ) => {
467+ await reconfigureServer ( { routeAllowList : [ 'batch' ] } ) ;
468+ const response = await restRequest ( {
469+ method : 'POST' ,
470+ headers : {
471+ 'Content-Type' : 'application/json' ,
472+ 'X-Parse-Application-Id' : 'test' ,
473+ 'X-Parse-Master-Key' : 'test' ,
474+ } ,
475+ url : 'http://localhost:8378/1/batch' ,
476+ body : JSON . stringify ( {
477+ requests : [ { method : 'POST' , path : '/1/classes/Blocked' , body : { x : 1 } } ] ,
478+ } ) ,
479+ } ) ;
480+ expect ( response . data . length ) . toBe ( 1 ) ;
481+ expect ( response . data [ 0 ] . success . objectId ) . toBeDefined ( ) ;
482+ } ) ;
483+ } ) ;
484+
378485 it_id ( '229cab22-dad3-4d08-8de5-64d813658596' ) ( it ) ( 'should block all route groups when not in allow list' , async ( ) => {
379486 await reconfigureServer ( {
380487 routeAllowList : [ 'classes/GameScore' ] ,
0 commit comments