@@ -614,3 +614,104 @@ describe('RestQuery.each', () => {
614614 ] ) ;
615615 } ) ;
616616} ) ;
617+
618+ describe ( 'redirectClassNameForKey security' , ( ) => {
619+ let config ;
620+
621+ beforeEach ( ( ) => {
622+ config = Config . get ( 'test' ) ;
623+ } ) ;
624+
625+ it ( 'should scope _Session results to the current user when redirected via redirectClassNameForKey' , async ( ) => {
626+ // Create two users with sessions (without logging out, to preserve sessions)
627+ const user1 = await Parse . User . signUp ( 'user1' , 'password1' ) ;
628+ const sessionToken1 = user1 . getSessionToken ( ) ;
629+
630+ // Sign up user2 via REST to avoid logging out user1
631+ await request ( {
632+ method : 'POST' ,
633+ url : Parse . serverURL + '/users' ,
634+ headers : {
635+ 'X-Parse-Application-Id' : Parse . applicationId ,
636+ 'X-Parse-REST-API-Key' : 'rest' ,
637+ 'Content-Type' : 'application/json' ,
638+ } ,
639+ body : { username : 'user2' , password : 'password2' } ,
640+ } ) ;
641+
642+ // Create a public class with a relation field pointing to _Session
643+ // (using masterKey to create the object and relation schema)
644+ const obj = new Parse . Object ( 'PublicData' ) ;
645+ const relation = obj . relation ( 'pivot' ) ;
646+ // Add a fake pointer to _Session to establish the relation schema
647+ relation . add ( Parse . Object . fromJSON ( { className : '_Session' , objectId : 'fakeId' } ) ) ;
648+ await obj . save ( null , { useMasterKey : true } ) ;
649+
650+ // Authenticated user queries with redirectClassNameForKey
651+ const userAuth = await auth . getAuthForSessionToken ( {
652+ config,
653+ sessionToken : sessionToken1 ,
654+ } ) ;
655+ const result = await rest . find ( config , userAuth , 'PublicData' , { } , { redirectClassNameForKey : 'pivot' } ) ;
656+
657+ // Should only see user1's own session, not user2's
658+ expect ( result . results . length ) . toBe ( 1 ) ;
659+ expect ( result . results [ 0 ] . user . objectId ) . toBe ( user1 . id ) ;
660+ } ) ;
661+
662+ it ( 'should reject unauthenticated access to _Session via redirectClassNameForKey' , async ( ) => {
663+ // Create a user so a session exists
664+ await Parse . User . signUp ( 'victim' , 'password123' ) ;
665+ await Parse . User . logOut ( ) ;
666+
667+ // Create a public class with a relation to _Session
668+ const obj = new Parse . Object ( 'PublicData' ) ;
669+ const relation = obj . relation ( 'pivot' ) ;
670+ relation . add ( Parse . Object . fromJSON ( { className : '_Session' , objectId : 'fakeId' } ) ) ;
671+ await obj . save ( null , { useMasterKey : true } ) ;
672+
673+ // Unauthenticated query with redirectClassNameForKey
674+ await expectAsync (
675+ rest . find ( config , auth . nobody ( config ) , 'PublicData' , { } , { redirectClassNameForKey : 'pivot' } )
676+ ) . toBeRejectedWith (
677+ jasmine . objectContaining ( { code : Parse . Error . INVALID_SESSION_TOKEN } )
678+ ) ;
679+ } ) ;
680+
681+ it ( 'should block redirectClassNameForKey to master-only classes' , async ( ) => {
682+ // Create a public class with a relation to _JobStatus (master-only)
683+ const obj = new Parse . Object ( 'PublicData' ) ;
684+ const relation = obj . relation ( 'jobPivot' ) ;
685+ relation . add ( Parse . Object . fromJSON ( { className : '_JobStatus' , objectId : 'fakeId' } ) ) ;
686+ await obj . save ( null , { useMasterKey : true } ) ;
687+
688+ // Create a user for authenticated access
689+ const user = await Parse . User . signUp ( 'attacker' , 'password123' ) ;
690+ const sessionToken = user . getSessionToken ( ) ;
691+ const userAuth = await auth . getAuthForSessionToken ( { config, sessionToken } ) ;
692+
693+ // Authenticated query should be blocked
694+ await expectAsync (
695+ rest . find ( config , userAuth , 'PublicData' , { } , { redirectClassNameForKey : 'jobPivot' } )
696+ ) . toBeRejectedWith (
697+ jasmine . objectContaining ( { code : Parse . Error . OPERATION_FORBIDDEN } )
698+ ) ;
699+ } ) ;
700+
701+ it ( 'should allow redirectClassNameForKey between regular classes' , async ( ) => {
702+ // Create target class objects
703+ const wheel1 = new Parse . Object ( 'Wheel' ) ;
704+ await wheel1 . save ( ) ;
705+
706+ // Create source class with relation to Wheel
707+ const car = new Parse . Object ( 'Car' ) ;
708+ const relation = car . relation ( 'wheels' ) ;
709+ relation . add ( wheel1 ) ;
710+ await car . save ( ) ;
711+
712+ // Query with redirectClassNameForKey should work normally
713+ const result = await rest . find ( config , auth . nobody ( config ) , 'Car' , { } , { redirectClassNameForKey : 'wheels' } ) ;
714+ expect ( result . results . length ) . toBe ( 1 ) ;
715+ expect ( result . results [ 0 ] . objectId ) . toBe ( wheel1 . id ) ;
716+ } ) ;
717+ } ) ;
0 commit comments