@@ -5405,4 +5405,133 @@ describe('Vulnerabilities', () => {
54055405 } ) ;
54065406 } ) ;
54075407 } ) ;
5408+
5409+ describe ( '(GHSA-445j-ww4h-339m) Cloud Code trigger context prototype poisoning via X-Parse-Cloud-Context header' , ( ) => {
5410+ const headers = {
5411+ 'Content-Type' : 'application/json' ,
5412+ 'X-Parse-Application-Id' : 'test' ,
5413+ 'X-Parse-REST-API-Key' : 'rest' ,
5414+ } ;
5415+
5416+ it ( 'accepts __proto__ in X-Parse-Cloud-Context header' , async ( ) => {
5417+ // Context is client-controlled metadata for Cloud Code triggers and is not subject
5418+ // to requestKeywordDenylist. The __proto__ key is allowed but must not cause
5419+ // prototype pollution (verified by separate tests below).
5420+ Parse . Cloud . beforeSave ( 'ContextTest' , ( ) => { } ) ;
5421+ const response = await request ( {
5422+ headers : {
5423+ ...headers ,
5424+ 'X-Parse-Cloud-Context' : JSON . stringify (
5425+ JSON . parse ( '{"__proto__": {"isAdmin": true}}' )
5426+ ) ,
5427+ } ,
5428+ method : 'POST' ,
5429+ url : 'http://localhost:8378/1/classes/ContextTest' ,
5430+ body : JSON . stringify ( { foo : 'bar' } ) ,
5431+ } ) . catch ( e => e ) ;
5432+ expect ( response . status ) . toBe ( 201 ) ;
5433+ } ) ;
5434+
5435+ it ( 'accepts constructor in X-Parse-Cloud-Context header' , async ( ) => {
5436+ Parse . Cloud . beforeSave ( 'ContextTest' , ( ) => { } ) ;
5437+ const response = await request ( {
5438+ headers : {
5439+ ...headers ,
5440+ 'X-Parse-Cloud-Context' : JSON . stringify ( { constructor : { prototype : { dummy : 0 } } } ) ,
5441+ } ,
5442+ method : 'POST' ,
5443+ url : 'http://localhost:8378/1/classes/ContextTest' ,
5444+ body : JSON . stringify ( { foo : 'bar' } ) ,
5445+ } ) . catch ( e => e ) ;
5446+ expect ( response . status ) . toBe ( 201 ) ;
5447+ expect ( Object . prototype . dummy ) . toBeUndefined ( ) ;
5448+ } ) ;
5449+
5450+ it ( 'accepts __proto__ in _context body field' , async ( ) => {
5451+ Parse . Cloud . beforeSave ( 'ContextTest' , ( ) => { } ) ;
5452+ const response = await request ( {
5453+ method : 'POST' ,
5454+ url : 'http://localhost:8378/1/classes/ContextTest' ,
5455+ headers : {
5456+ 'X-Parse-REST-API-Key' : 'rest' ,
5457+ } ,
5458+ body : {
5459+ foo : 'bar' ,
5460+ _ApplicationId : 'test' ,
5461+ _context : JSON . stringify ( JSON . parse ( '{"__proto__": {"isAdmin": true}}' ) ) ,
5462+ } ,
5463+ } ) . catch ( e => e ) ;
5464+ expect ( response . status ) . toBe ( 201 ) ;
5465+ } ) ;
5466+
5467+ it ( 'does not pollute request.context prototype via X-Parse-Cloud-Context header' , async ( ) => {
5468+ let contextInTrigger ;
5469+ Parse . Cloud . beforeSave ( 'ContextTest' , req => {
5470+ contextInTrigger = req . context ;
5471+ } ) ;
5472+ await request ( {
5473+ headers : {
5474+ ...headers ,
5475+ 'X-Parse-Cloud-Context' : JSON . stringify (
5476+ JSON . parse ( '{"__proto__": {"isAdmin": true}}' )
5477+ ) ,
5478+ } ,
5479+ method : 'POST' ,
5480+ url : 'http://localhost:8378/1/classes/ContextTest' ,
5481+ body : JSON . stringify ( { foo : 'bar' } ) ,
5482+ } ) . catch ( e => e ) ;
5483+ // Verify prototype was not polluted
5484+ expect ( contextInTrigger ?. isAdmin ) . toBeUndefined ( ) ;
5485+ expect ( Object . getPrototypeOf ( contextInTrigger || { } ) ) . not . toEqual (
5486+ jasmine . objectContaining ( { isAdmin : true } )
5487+ ) ;
5488+ } ) ;
5489+
5490+ it ( 'does not pollute request.context prototype via _context body field' , async ( ) => {
5491+ let contextInTrigger ;
5492+ Parse . Cloud . beforeSave ( 'ContextTest' , req => {
5493+ contextInTrigger = req . context ;
5494+ } ) ;
5495+ await request ( {
5496+ method : 'POST' ,
5497+ url : 'http://localhost:8378/1/classes/ContextTest' ,
5498+ headers : {
5499+ 'X-Parse-REST-API-Key' : 'rest' ,
5500+ } ,
5501+ body : {
5502+ foo : 'bar' ,
5503+ _ApplicationId : 'test' ,
5504+ _context : JSON . stringify ( JSON . parse ( '{"__proto__": {"isAdmin": true}}' ) ) ,
5505+ } ,
5506+ } ) . catch ( e => e ) ;
5507+ // Verify prototype was not polluted
5508+ expect ( contextInTrigger ?. isAdmin ) . toBeUndefined ( ) ;
5509+ expect ( Object . getPrototypeOf ( contextInTrigger || { } ) ) . not . toEqual (
5510+ jasmine . objectContaining ( { isAdmin : true } )
5511+ ) ;
5512+ } ) ;
5513+
5514+ it ( 'does not allow prototype-polluted properties to survive deletion in trigger context' , async ( ) => {
5515+ // This test verifies that __proto__ pollution cannot bypass context property deletion.
5516+ // When a developer deletes a context property, prototype-polluted properties would
5517+ // survive the deletion (unlike directly set properties), creating a security gap.
5518+ let contextAfterDelete ;
5519+ Parse . Cloud . beforeSave ( 'ContextTest' , req => {
5520+ delete req . context . isAdmin ;
5521+ contextAfterDelete = { isAdmin : req . context . isAdmin } ;
5522+ } ) ;
5523+ await request ( {
5524+ headers : {
5525+ ...headers ,
5526+ 'X-Parse-Cloud-Context' : JSON . stringify (
5527+ JSON . parse ( '{"__proto__": {"isAdmin": true}}' )
5528+ ) ,
5529+ } ,
5530+ method : 'POST' ,
5531+ url : 'http://localhost:8378/1/classes/ContextTest' ,
5532+ body : JSON . stringify ( { foo : 'bar' } ) ,
5533+ } ) . catch ( e => e ) ;
5534+ expect ( contextAfterDelete ?. isAdmin ) . toBeUndefined ( ) ;
5535+ } ) ;
5536+ } ) ;
54085537} ) ;
0 commit comments