@@ -353,12 +353,23 @@ describe('Vulnerabilities', () => {
353353 } ) ;
354354
355355 it ( 'denies __proto__ after a sibling nested object' , async ( ) => {
356- // Cannot test via HTTP because deepcopy() strips __proto__ before the denylist
357- // check runs. Test objectContainsKeyValue directly with a JSON.parse'd object
358- // that preserves __proto__ as an own property.
359- const Utils = require ( '../lib/Utils' ) ;
360- const data = JSON . parse ( '{"profile": {"name": "alice"}, "__proto__": {"isAdmin": true}}' ) ;
361- expect ( Utils . objectContainsKeyValue ( data , '__proto__' , undefined ) ) . toBe ( true ) ;
356+ const headers = {
357+ 'Content-Type' : 'application/json' ,
358+ 'X-Parse-Application-Id' : 'test' ,
359+ 'X-Parse-REST-API-Key' : 'rest' ,
360+ } ;
361+ const response = await request ( {
362+ headers,
363+ method : 'POST' ,
364+ url : 'http://localhost:8378/1/classes/PP' ,
365+ body : JSON . stringify (
366+ JSON . parse ( '{"profile": {"name": "alice"}, "__proto__": {"isAdmin": true}}' )
367+ ) ,
368+ } ) . catch ( e => e ) ;
369+ expect ( response . status ) . toBe ( 400 ) ;
370+ const text = typeof response . data === 'string' ? JSON . parse ( response . data ) : response . data ;
371+ expect ( text . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
372+ expect ( text . error ) . toContain ( '__proto__' ) ;
362373 } ) ;
363374
364375 it ( 'denies constructor after a sibling nested object' , async ( ) => {
@@ -2644,4 +2655,95 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na
26442655 expect ( result . body . errors [ 0 ] . message ) . toMatch ( / e x c e e d s m a x i m u m a l l o w e d / ) ;
26452656 } ) ;
26462657 } ) ;
2658+
2659+ describe ( '(GHSA-9ccr-fpp6-78qf) Schema poisoning via __proto__ bypassing requestKeywordDenylist and addField CLP' , ( ) => {
2660+ const headers = {
2661+ 'Content-Type' : 'application/json' ,
2662+ 'X-Parse-Application-Id' : 'test' ,
2663+ 'X-Parse-REST-API-Key' : 'rest' ,
2664+ } ;
2665+
2666+ it ( 'rejects __proto__ in request body via HTTP' , async ( ) => {
2667+ const response = await request ( {
2668+ headers,
2669+ method : 'POST' ,
2670+ url : 'http://localhost:8378/1/classes/ProtoTest' ,
2671+ body : JSON . stringify ( JSON . parse ( '{"name":"test","__proto__":{"injected":"value"}}' ) ) ,
2672+ } ) . catch ( e => e ) ;
2673+ expect ( response . status ) . toBe ( 400 ) ;
2674+ const text = typeof response . data === 'string' ? JSON . parse ( response . data ) : response . data ;
2675+ expect ( text . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2676+ expect ( text . error ) . toContain ( '__proto__' ) ;
2677+ } ) ;
2678+
2679+ it ( 'does not add fields to a locked schema via __proto__' , async ( ) => {
2680+ const schema = new Parse . Schema ( 'LockedSchema' ) ;
2681+ schema . addString ( 'name' ) ;
2682+ schema . setCLP ( {
2683+ find : { '*' : true } ,
2684+ get : { '*' : true } ,
2685+ create : { '*' : true } ,
2686+ update : { '*' : true } ,
2687+ delete : { '*' : true } ,
2688+ addField : { } ,
2689+ } ) ;
2690+ await schema . save ( ) ;
2691+
2692+ // Attempt to inject a field via __proto__
2693+ const response = await request ( {
2694+ headers,
2695+ method : 'POST' ,
2696+ url : 'http://localhost:8378/1/classes/LockedSchema' ,
2697+ body : JSON . stringify ( JSON . parse ( '{"name":"test","__proto__":{"newField":"bypassed"}}' ) ) ,
2698+ } ) . catch ( e => e ) ;
2699+
2700+ // Should be rejected by denylist
2701+ expect ( response . status ) . toBe ( 400 ) ;
2702+
2703+ // Verify schema was not modified
2704+ const schemaResponse = await request ( {
2705+ headers : {
2706+ 'X-Parse-Application-Id' : 'test' ,
2707+ 'X-Parse-Master-Key' : 'test' ,
2708+ } ,
2709+ method : 'GET' ,
2710+ url : 'http://localhost:8378/1/schemas/LockedSchema' ,
2711+ } ) ;
2712+ const fields = schemaResponse . data . fields ;
2713+ expect ( fields . newField ) . toBeUndefined ( ) ;
2714+ } ) ;
2715+
2716+ it ( 'does not cause schema type conflict via __proto__' , async ( ) => {
2717+ const schema = new Parse . Schema ( 'TypeConflict' ) ;
2718+ schema . addString ( 'name' ) ;
2719+ schema . addString ( 'score' ) ;
2720+ schema . setCLP ( {
2721+ find : { '*' : true } ,
2722+ get : { '*' : true } ,
2723+ create : { '*' : true } ,
2724+ update : { '*' : true } ,
2725+ delete : { '*' : true } ,
2726+ addField : { } ,
2727+ } ) ;
2728+ await schema . save ( ) ;
2729+
2730+ // Attempt to inject 'score' as Number via __proto__
2731+ const response = await request ( {
2732+ headers,
2733+ method : 'POST' ,
2734+ url : 'http://localhost:8378/1/classes/TypeConflict' ,
2735+ body : JSON . stringify ( JSON . parse ( '{"name":"test","__proto__":{"score":42}}' ) ) ,
2736+ } ) . catch ( e => e ) ;
2737+
2738+ // Should be rejected by denylist
2739+ expect ( response . status ) . toBe ( 400 ) ;
2740+
2741+ // Verify 'score' field is still String type
2742+ const obj = new Parse . Object ( 'TypeConflict' ) ;
2743+ obj . set ( 'name' , 'valid' ) ;
2744+ obj . set ( 'score' , 'string-value' ) ;
2745+ await obj . save ( ) ;
2746+ expect ( obj . get ( 'score' ) ) . toBe ( 'string-value' ) ;
2747+ } ) ;
2748+ } ) ;
26472749} ) ;
0 commit comments