@@ -2307,6 +2307,207 @@ describe('Vulnerabilities', () => {
23072307 } ) ;
23082308 } ) ;
23092309
2310+ describe ( '(GHSA-wmwx-jr2p-4j4r) $relatedTo bypasses protectedFields and parent ACL for Relation fields' , ( ) => {
2311+ let childLinked ;
2312+ let parentProtectedKey ;
2313+ let parentPrivate ;
2314+ let parentPublic ;
2315+
2316+ const relatedToWhere = ( parentId , key , extra = { } ) => ( {
2317+ $relatedTo : {
2318+ object : { __type : 'Pointer' , className : 'RelParent' , objectId : parentId } ,
2319+ key,
2320+ } ,
2321+ ...extra ,
2322+ } ) ;
2323+
2324+ const queryChild = ( where , headers = { } ) =>
2325+ request ( {
2326+ method : 'GET' ,
2327+ url : `${ Parse . serverURL } /classes/RelChild` ,
2328+ headers : {
2329+ 'X-Parse-Application-Id' : Parse . applicationId ,
2330+ 'X-Parse-REST-API-Key' : 'rest' ,
2331+ ...headers ,
2332+ } ,
2333+ qs : { where : JSON . stringify ( where ) } ,
2334+ } ) . catch ( e => e ) ;
2335+
2336+ beforeEach ( async ( ) => {
2337+ const schema = new Parse . Schema ( 'RelParent' ) ;
2338+ schema . addString ( 'name' ) ;
2339+ schema . addRelation ( 'secretRel' , 'RelChild' ) ;
2340+ schema . addRelation ( 'openRel' , 'RelChild' ) ;
2341+ schema . setCLP ( {
2342+ find : { '*' : true } ,
2343+ get : { '*' : true } ,
2344+ create : { '*' : true } ,
2345+ update : { '*' : true } ,
2346+ delete : { '*' : true } ,
2347+ addField : { } ,
2348+ // secretRel is a protected Relation field for public clients
2349+ protectedFields : { '*' : [ 'secretRel' ] } ,
2350+ } ) ;
2351+ await schema . save ( ) ;
2352+
2353+ childLinked = new Parse . Object ( 'RelChild' , { value : 'linked child' } ) ;
2354+ await childLinked . save ( null , { useMasterKey : true } ) ;
2355+
2356+ const publicAcl = new Parse . ACL ( ) ;
2357+ publicAcl . setPublicReadAccess ( true ) ;
2358+
2359+ const privateAcl = new Parse . ACL ( ) ;
2360+ privateAcl . setPublicReadAccess ( false ) ;
2361+ privateAcl . setPublicWriteAccess ( false ) ;
2362+
2363+ // Publicly readable parent whose relation key is protected (isolates the
2364+ // protectedFields facet).
2365+ parentProtectedKey = new Parse . Object ( 'RelParent' , { name : 'protected-key parent' } ) ;
2366+ parentProtectedKey . setACL ( publicAcl ) ;
2367+ parentProtectedKey . relation ( 'secretRel' ) . add ( childLinked ) ;
2368+ await parentProtectedKey . save ( null , { useMasterKey : true } ) ;
2369+
2370+ // Parent that is not readable by the public, queried via a non-protected
2371+ // relation key (isolates the parent-ACL facet).
2372+ parentPrivate = new Parse . Object ( 'RelParent' , { name : 'private parent' } ) ;
2373+ parentPrivate . setACL ( privateAcl ) ;
2374+ parentPrivate . relation ( 'openRel' ) . add ( childLinked ) ;
2375+ await parentPrivate . save ( null , { useMasterKey : true } ) ;
2376+
2377+ // Publicly readable parent with a non-protected relation key (legitimate
2378+ // use that must keep working).
2379+ parentPublic = new Parse . Object ( 'RelParent' , { name : 'public parent' } ) ;
2380+ parentPublic . setACL ( publicAcl ) ;
2381+ parentPublic . relation ( 'openRel' ) . add ( childLinked ) ;
2382+ await parentPublic . save ( null , { useMasterKey : true } ) ;
2383+ } ) ;
2384+
2385+ it ( 'denies $relatedTo query that references a protected relation field' , async ( ) => {
2386+ const res = await queryChild ( relatedToWhere ( parentProtectedKey . id , 'secretRel' ) ) ;
2387+ expect ( res . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
2388+ expect ( res . data . error ) . toBe ( 'Permission denied' ) ;
2389+ } ) ;
2390+
2391+ it ( 'denies $relatedTo on a protected relation field nested in $or' , async ( ) => {
2392+ const res = await queryChild ( {
2393+ $or : [ relatedToWhere ( parentProtectedKey . id , 'secretRel' ) ] ,
2394+ } ) ;
2395+ expect ( res . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
2396+ expect ( res . data . error ) . toBe ( 'Permission denied' ) ;
2397+ } ) ;
2398+
2399+ it ( 'denies $relatedTo on a protected relation field nested in $and' , async ( ) => {
2400+ const res = await queryChild ( {
2401+ $and : [ relatedToWhere ( parentProtectedKey . id , 'secretRel' ) ] ,
2402+ } ) ;
2403+ expect ( res . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
2404+ expect ( res . data . error ) . toBe ( 'Permission denied' ) ;
2405+ } ) ;
2406+
2407+ it ( 'denies $relatedTo on a protected relation field nested in $nor' , async ( ) => {
2408+ const res = await queryChild ( {
2409+ $nor : [ relatedToWhere ( parentProtectedKey . id , 'secretRel' ) ] ,
2410+ } ) ;
2411+ expect ( res . data . code ) . toBe ( Parse . Error . OPERATION_FORBIDDEN ) ;
2412+ expect ( res . data . error ) . toBe ( 'Permission denied' ) ;
2413+ } ) ;
2414+
2415+ it ( 'returns no results when the owning object is not readable by the caller' , async ( ) => {
2416+ const res = await queryChild ( relatedToWhere ( parentPrivate . id , 'openRel' ) ) ;
2417+ expect ( res . data . results ) . toEqual ( [ ] ) ;
2418+ } ) ;
2419+
2420+ it ( 'does not act as a membership oracle for an unreadable owning object' , async ( ) => {
2421+ const res = await queryChild (
2422+ relatedToWhere ( parentPrivate . id , 'openRel' , { objectId : childLinked . id } )
2423+ ) ;
2424+ expect ( res . data . results ) . toEqual ( [ ] ) ;
2425+ } ) ;
2426+
2427+ it ( 'still returns related objects for a readable parent and non-protected key' , async ( ) => {
2428+ const res = await queryChild ( relatedToWhere ( parentPublic . id , 'openRel' ) ) ;
2429+ expect ( res . data . results . length ) . toBe ( 1 ) ;
2430+ expect ( res . data . results [ 0 ] . objectId ) . toBe ( childLinked . id ) ;
2431+ } ) ;
2432+
2433+ it ( 'allows master key to query a protected relation and an unreadable parent' , async ( ) => {
2434+ const masterHeaders = { 'X-Parse-Master-Key' : Parse . masterKey } ;
2435+ const resProtected = await queryChild (
2436+ relatedToWhere ( parentProtectedKey . id , 'secretRel' ) ,
2437+ masterHeaders
2438+ ) ;
2439+ expect ( resProtected . data . results . length ) . toBe ( 1 ) ;
2440+ const resPrivate = await queryChild (
2441+ relatedToWhere ( parentPrivate . id , 'openRel' ) ,
2442+ masterHeaders
2443+ ) ;
2444+ expect ( resPrivate . data . results . length ) . toBe ( 1 ) ;
2445+ } ) ;
2446+
2447+ it ( 'respects user-level read access to the owning object' , async ( ) => {
2448+ const userA = await Parse . User . signUp ( 'relUserA' , 'pw' ) ;
2449+ const userB = await Parse . User . signUp ( 'relUserB' , 'pw' ) ;
2450+
2451+ const acl = new Parse . ACL ( ) ;
2452+ acl . setReadAccess ( userA , true ) ;
2453+ const parent = new Parse . Object ( 'RelParent' , { name : 'user-scoped parent' } ) ;
2454+ parent . setACL ( acl ) ;
2455+ parent . relation ( 'openRel' ) . add ( childLinked ) ;
2456+ await parent . save ( null , { useMasterKey : true } ) ;
2457+
2458+ const resA = await queryChild ( relatedToWhere ( parent . id , 'openRel' ) , {
2459+ 'X-Parse-Session-Token' : userA . getSessionToken ( ) ,
2460+ } ) ;
2461+ expect ( resA . data . results . length ) . toBe ( 1 ) ;
2462+
2463+ const resB = await queryChild ( relatedToWhere ( parent . id , 'openRel' ) , {
2464+ 'X-Parse-Session-Token' : userB . getSessionToken ( ) ,
2465+ } ) ;
2466+ expect ( resB . data . results ) . toEqual ( [ ] ) ;
2467+ } ) ;
2468+
2469+ it ( 'returns no results when the owning class denies get permission (CLP)' , async ( ) => {
2470+ // Owning class denies public `get`, so the owning-object read throws
2471+ // OPERATION_FORBIDDEN; the relation must then return no results.
2472+ const schema = new Parse . Schema ( 'RelParentNoGet' ) ;
2473+ schema . addRelation ( 'members' , 'RelChild' ) ;
2474+ schema . setCLP ( {
2475+ find : { '*' : true } ,
2476+ get : { } ,
2477+ create : { '*' : true } ,
2478+ update : { '*' : true } ,
2479+ delete : { '*' : true } ,
2480+ addField : { } ,
2481+ } ) ;
2482+ await schema . save ( ) ;
2483+
2484+ const acl = new Parse . ACL ( ) ;
2485+ acl . setPublicReadAccess ( true ) ;
2486+ const parent = new Parse . Object ( 'RelParentNoGet' , { name : 'no-get parent' } ) ;
2487+ parent . setACL ( acl ) ;
2488+ parent . relation ( 'members' ) . add ( childLinked ) ;
2489+ await parent . save ( null , { useMasterKey : true } ) ;
2490+
2491+ const res = await request ( {
2492+ method : 'GET' ,
2493+ url : `${ Parse . serverURL } /classes/RelChild` ,
2494+ headers : {
2495+ 'X-Parse-Application-Id' : Parse . applicationId ,
2496+ 'X-Parse-REST-API-Key' : 'rest' ,
2497+ } ,
2498+ qs : {
2499+ where : JSON . stringify ( {
2500+ $relatedTo : {
2501+ object : { __type : 'Pointer' , className : 'RelParentNoGet' , objectId : parent . id } ,
2502+ key : 'members' ,
2503+ } ,
2504+ } ) ,
2505+ } ,
2506+ } ) . catch ( e => e ) ;
2507+ expect ( res . data . results ) . toEqual ( [ ] ) ;
2508+ } ) ;
2509+ } ) ;
2510+
23102511 describe ( '(GHSA-j7mm-f4rv-6q6q) Protected fields bypass via LiveQuery dot-notation WHERE' , ( ) => {
23112512 let obj ;
23122513
0 commit comments