@@ -160,7 +160,228 @@ describe('Vulnerabilities', () => {
160160 } ) ;
161161 } ) ;
162162
163+ describe ( '(GHSA-5j86-7r7m-p8h6) Cloud function name prototype chain bypass' , ( ) => {
164+ const headers = {
165+ 'Content-Type' : 'application/json' ,
166+ 'X-Parse-Application-Id' : 'test' ,
167+ 'X-Parse-REST-API-Key' : 'rest' ,
168+ } ;
169+
170+ it ( 'rejects "constructor" as cloud function name' , async ( ) => {
171+ const response = await request ( {
172+ headers,
173+ method : 'POST' ,
174+ url : 'http://localhost:8378/1/functions/constructor' ,
175+ body : JSON . stringify ( { } ) ,
176+ } ) . catch ( e => e ) ;
177+ expect ( response . status ) . toBe ( 400 ) ;
178+ const text = JSON . parse ( response . text ) ;
179+ expect ( text . code ) . toBe ( Parse . Error . SCRIPT_FAILED ) ;
180+ expect ( text . error ) . toContain ( 'Invalid function' ) ;
181+ } ) ;
182+
183+ it ( 'rejects "toString" as cloud function name' , async ( ) => {
184+ const response = await request ( {
185+ headers,
186+ method : 'POST' ,
187+ url : 'http://localhost:8378/1/functions/toString' ,
188+ body : JSON . stringify ( { } ) ,
189+ } ) . catch ( e => e ) ;
190+ expect ( response . status ) . toBe ( 400 ) ;
191+ const text = JSON . parse ( response . text ) ;
192+ expect ( text . code ) . toBe ( Parse . Error . SCRIPT_FAILED ) ;
193+ expect ( text . error ) . toContain ( 'Invalid function' ) ;
194+ } ) ;
195+
196+ it ( 'rejects "valueOf" as cloud function name' , async ( ) => {
197+ const response = await request ( {
198+ headers,
199+ method : 'POST' ,
200+ url : 'http://localhost:8378/1/functions/valueOf' ,
201+ body : JSON . stringify ( { } ) ,
202+ } ) . catch ( e => e ) ;
203+ expect ( response . status ) . toBe ( 400 ) ;
204+ const text = JSON . parse ( response . text ) ;
205+ expect ( text . code ) . toBe ( Parse . Error . SCRIPT_FAILED ) ;
206+ expect ( text . error ) . toContain ( 'Invalid function' ) ;
207+ } ) ;
208+
209+ it ( 'rejects "hasOwnProperty" as cloud function name' , async ( ) => {
210+ const response = await request ( {
211+ headers,
212+ method : 'POST' ,
213+ url : 'http://localhost:8378/1/functions/hasOwnProperty' ,
214+ body : JSON . stringify ( { } ) ,
215+ } ) . catch ( e => e ) ;
216+ expect ( response . status ) . toBe ( 400 ) ;
217+ const text = JSON . parse ( response . text ) ;
218+ expect ( text . code ) . toBe ( Parse . Error . SCRIPT_FAILED ) ;
219+ expect ( text . error ) . toContain ( 'Invalid function' ) ;
220+ } ) ;
221+
222+ it ( 'rejects "__proto__.toString" as cloud function name' , async ( ) => {
223+ const response = await request ( {
224+ headers,
225+ method : 'POST' ,
226+ url : 'http://localhost:8378/1/functions/__proto__.toString' ,
227+ body : JSON . stringify ( { } ) ,
228+ } ) . catch ( e => e ) ;
229+ expect ( response . status ) . toBe ( 400 ) ;
230+ const text = JSON . parse ( response . text ) ;
231+ expect ( text . code ) . toBe ( Parse . Error . SCRIPT_FAILED ) ;
232+ expect ( text . error ) . toContain ( 'Invalid function' ) ;
233+ } ) ;
234+
235+ it ( 'still executes a legitimately defined cloud function' , async ( ) => {
236+ Parse . Cloud . define ( 'legitimateFunction' , ( ) => 'hello' ) ;
237+ const response = await request ( {
238+ headers,
239+ method : 'POST' ,
240+ url : 'http://localhost:8378/1/functions/legitimateFunction' ,
241+ body : JSON . stringify ( { } ) ,
242+ } ) ;
243+ expect ( response . status ) . toBe ( 200 ) ;
244+ expect ( JSON . parse ( response . text ) . result ) . toBe ( 'hello' ) ;
245+ } ) ;
246+ } ) ;
247+
163248 describe ( 'Request denylist' , ( ) => {
249+ describe ( '(GHSA-q342-9w2p-57fp) Denylist bypass via sibling nested objects' , ( ) => {
250+ it ( 'denies _bsontype:Code after a sibling nested object' , async ( ) => {
251+ const headers = {
252+ 'Content-Type' : 'application/json' ,
253+ 'X-Parse-Application-Id' : 'test' ,
254+ 'X-Parse-REST-API-Key' : 'rest' ,
255+ } ;
256+ const response = await request ( {
257+ headers,
258+ method : 'POST' ,
259+ url : 'http://localhost:8378/1/classes/Bypass' ,
260+ body : JSON . stringify ( {
261+ obj : {
262+ metadata : { } ,
263+ _bsontype : 'Code' ,
264+ code : 'malicious' ,
265+ } ,
266+ } ) ,
267+ } ) . catch ( e => e ) ;
268+ expect ( response . status ) . toBe ( 400 ) ;
269+ const text = JSON . parse ( response . text ) ;
270+ expect ( text . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
271+ expect ( text . error ) . toBe (
272+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
273+ ) ;
274+ } ) ;
275+
276+ it ( 'denies _bsontype:Code after a sibling nested array' , async ( ) => {
277+ const headers = {
278+ 'Content-Type' : 'application/json' ,
279+ 'X-Parse-Application-Id' : 'test' ,
280+ 'X-Parse-REST-API-Key' : 'rest' ,
281+ } ;
282+ const response = await request ( {
283+ headers,
284+ method : 'POST' ,
285+ url : 'http://localhost:8378/1/classes/Bypass' ,
286+ body : JSON . stringify ( {
287+ obj : {
288+ tags : [ 'safe' ] ,
289+ _bsontype : 'Code' ,
290+ code : 'malicious' ,
291+ } ,
292+ } ) ,
293+ } ) . catch ( e => e ) ;
294+ expect ( response . status ) . toBe ( 400 ) ;
295+ const text = JSON . parse ( response . text ) ;
296+ expect ( text . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
297+ expect ( text . error ) . toBe (
298+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
299+ ) ;
300+ } ) ;
301+
302+ it ( 'denies __proto__ after a sibling nested object' , async ( ) => {
303+ // Cannot test via HTTP because deepcopy() strips __proto__ before the denylist
304+ // check runs. Test objectContainsKeyValue directly with a JSON.parse'd object
305+ // that preserves __proto__ as an own property.
306+ const Utils = require ( '../lib/Utils' ) ;
307+ const data = JSON . parse ( '{"profile": {"name": "alice"}, "__proto__": {"isAdmin": true}}' ) ;
308+ expect ( Utils . objectContainsKeyValue ( data , '__proto__' , undefined ) ) . toBe ( true ) ;
309+ } ) ;
310+
311+ it ( 'denies constructor after a sibling nested object' , async ( ) => {
312+ const headers = {
313+ 'Content-Type' : 'application/json' ,
314+ 'X-Parse-Application-Id' : 'test' ,
315+ 'X-Parse-REST-API-Key' : 'rest' ,
316+ } ;
317+ const response = await request ( {
318+ headers,
319+ method : 'POST' ,
320+ url : 'http://localhost:8378/1/classes/Bypass' ,
321+ body : JSON . stringify ( {
322+ obj : {
323+ data : { } ,
324+ constructor : { prototype : { polluted : true } } ,
325+ } ,
326+ } ) ,
327+ } ) . catch ( e => e ) ;
328+ expect ( response . status ) . toBe ( 400 ) ;
329+ const text = JSON . parse ( response . text ) ;
330+ expect ( text . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
331+ expect ( text . error ) . toBe (
332+ 'Prohibited keyword in request data: {"key":"constructor"}.'
333+ ) ;
334+ } ) ;
335+
336+ it ( 'denies _bsontype:Code nested inside a second sibling object' , async ( ) => {
337+ const headers = {
338+ 'Content-Type' : 'application/json' ,
339+ 'X-Parse-Application-Id' : 'test' ,
340+ 'X-Parse-REST-API-Key' : 'rest' ,
341+ } ;
342+ const response = await request ( {
343+ headers,
344+ method : 'POST' ,
345+ url : 'http://localhost:8378/1/classes/Bypass' ,
346+ body : JSON . stringify ( {
347+ field1 : { safe : true } ,
348+ field2 : { _bsontype : 'Code' , code : 'malicious' } ,
349+ } ) ,
350+ } ) . catch ( e => e ) ;
351+ expect ( response . status ) . toBe ( 400 ) ;
352+ const text = JSON . parse ( response . text ) ;
353+ expect ( text . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
354+ expect ( text . error ) . toBe (
355+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
356+ ) ;
357+ } ) ;
358+
359+ it ( 'handles circular references without infinite loop' , ( ) => {
360+ const Utils = require ( '../lib/Utils' ) ;
361+ const obj = { name : 'test' , nested : { value : 1 } } ;
362+ obj . nested . self = obj ;
363+ expect ( Utils . objectContainsKeyValue ( obj , 'nonexistent' , undefined ) ) . toBe ( false ) ;
364+ } ) ;
365+
366+ it ( 'denies _bsontype:Code in file metadata after a sibling nested object' , async ( ) => {
367+ const str = 'Hello World!' ;
368+ const data = [ ] ;
369+ for ( let i = 0 ; i < str . length ; i ++ ) {
370+ data . push ( str . charCodeAt ( i ) ) ;
371+ }
372+ const file = new Parse . File ( 'hello.txt' , data , 'text/plain' ) ;
373+ file . addMetadata ( 'nested' , { safe : true } ) ;
374+ file . addMetadata ( '_bsontype' , 'Code' ) ;
375+ file . addMetadata ( 'code' , 'malicious' ) ;
376+ await expectAsync ( file . save ( ) ) . toBeRejectedWith (
377+ new Parse . Error (
378+ Parse . Error . INVALID_KEY_NAME ,
379+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
380+ )
381+ ) ;
382+ } ) ;
383+ } ) ;
384+
164385 it ( 'denies BSON type code data in write request by default' , async ( ) => {
165386 const headers = {
166387 'Content-Type' : 'application/json' ,
0 commit comments