@@ -1978,3 +1978,258 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint
19781978 }
19791979 } ) ;
19801980} ) ;
1981+
1982+ describe ( '(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field name in PostgreSQL adapter' , ( ) => {
1983+ const headers = {
1984+ 'Content-Type' : 'application/json' ,
1985+ 'X-Parse-Application-Id' : 'test' ,
1986+ 'X-Parse-REST-API-Key' : 'rest' ,
1987+ 'X-Parse-Master-Key' : 'test' ,
1988+ } ;
1989+ const serverURL = 'http://localhost:8378/1' ;
1990+
1991+ beforeEach ( async ( ) => {
1992+ const obj = new Parse . Object ( 'TestClass' ) ;
1993+ obj . set ( 'playerName' , 'Alice' ) ;
1994+ obj . set ( 'score' , 100 ) ;
1995+ await obj . save ( null , { useMasterKey : true } ) ;
1996+ } ) ;
1997+
1998+ it ( 'rejects field names containing double quotes in $regex query with master key' , async ( ) => {
1999+ const maliciousField = 'playerName" OR 1=1 --' ;
2000+ const response = await request ( {
2001+ method : 'GET' ,
2002+ url : `${ serverURL } /classes/TestClass` ,
2003+ headers,
2004+ qs : {
2005+ where : JSON . stringify ( {
2006+ [ maliciousField ] : { $regex : 'x' } ,
2007+ } ) ,
2008+ } ,
2009+ } ) . catch ( e => e ) ;
2010+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2011+ } ) ;
2012+
2013+ it ( 'rejects field names containing single quotes in $regex query with master key' , async ( ) => {
2014+ const maliciousField = "playerName' OR '1'='1" ;
2015+ const response = await request ( {
2016+ method : 'GET' ,
2017+ url : `${ serverURL } /classes/TestClass` ,
2018+ headers,
2019+ qs : {
2020+ where : JSON . stringify ( {
2021+ [ maliciousField ] : { $regex : 'x' } ,
2022+ } ) ,
2023+ } ,
2024+ } ) . catch ( e => e ) ;
2025+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2026+ } ) ;
2027+
2028+ it ( 'rejects field names containing semicolons in $regex query with master key' , async ( ) => {
2029+ const maliciousField = 'playerName; DROP TABLE "TestClass" --' ;
2030+ const response = await request ( {
2031+ method : 'GET' ,
2032+ url : `${ serverURL } /classes/TestClass` ,
2033+ headers,
2034+ qs : {
2035+ where : JSON . stringify ( {
2036+ [ maliciousField ] : { $regex : 'x' } ,
2037+ } ) ,
2038+ } ,
2039+ } ) . catch ( e => e ) ;
2040+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2041+ } ) ;
2042+
2043+ it ( 'rejects field names containing parentheses in $regex query with master key' , async ( ) => {
2044+ const maliciousField = 'playerName" ~ \'x\' OR (SELECT 1) --' ;
2045+ const response = await request ( {
2046+ method : 'GET' ,
2047+ url : `${ serverURL } /classes/TestClass` ,
2048+ headers,
2049+ qs : {
2050+ where : JSON . stringify ( {
2051+ [ maliciousField ] : { $regex : 'x' } ,
2052+ } ) ,
2053+ } ,
2054+ } ) . catch ( e => e ) ;
2055+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2056+ } ) ;
2057+
2058+ it ( 'allows legitimate $regex query with master key' , async ( ) => {
2059+ const response = await request ( {
2060+ method : 'GET' ,
2061+ url : `${ serverURL } /classes/TestClass` ,
2062+ headers,
2063+ qs : {
2064+ where : JSON . stringify ( {
2065+ playerName : { $regex : 'Ali' } ,
2066+ } ) ,
2067+ } ,
2068+ } ) ;
2069+ expect ( response . data . results . length ) . toBe ( 1 ) ;
2070+ expect ( response . data . results [ 0 ] . playerName ) . toBe ( 'Alice' ) ;
2071+ } ) ;
2072+
2073+ it ( 'allows legitimate $regex query with dot notation and master key' , async ( ) => {
2074+ const obj = new Parse . Object ( 'TestClass' ) ;
2075+ obj . set ( 'metadata' , { tag : 'hello-world' } ) ;
2076+ await obj . save ( null , { useMasterKey : true } ) ;
2077+ const response = await request ( {
2078+ method : 'GET' ,
2079+ url : `${ serverURL } /classes/TestClass` ,
2080+ headers,
2081+ qs : {
2082+ where : JSON . stringify ( {
2083+ 'metadata.tag' : { $regex : 'hello' } ,
2084+ } ) ,
2085+ } ,
2086+ } ) ;
2087+ expect ( response . data . results . length ) . toBe ( 1 ) ;
2088+ expect ( response . data . results [ 0 ] . metadata . tag ) . toBe ( 'hello-world' ) ;
2089+ } ) ;
2090+
2091+ it ( 'allows legitimate $regex query without master key' , async ( ) => {
2092+ const response = await request ( {
2093+ method : 'GET' ,
2094+ url : `${ serverURL } /classes/TestClass` ,
2095+ headers : {
2096+ 'Content-Type' : 'application/json' ,
2097+ 'X-Parse-Application-Id' : 'test' ,
2098+ 'X-Parse-REST-API-Key' : 'rest' ,
2099+ } ,
2100+ qs : {
2101+ where : JSON . stringify ( {
2102+ playerName : { $regex : 'Ali' } ,
2103+ } ) ,
2104+ } ,
2105+ } ) ;
2106+ expect ( response . data . results . length ) . toBe ( 1 ) ;
2107+ expect ( response . data . results [ 0 ] . playerName ) . toBe ( 'Alice' ) ;
2108+ } ) ;
2109+
2110+ it ( 'rejects field names with SQL injection via non-$regex operators with master key' , async ( ) => {
2111+ const maliciousField = 'playerName" OR 1=1 --' ;
2112+ const response = await request ( {
2113+ method : 'GET' ,
2114+ url : `${ serverURL } /classes/TestClass` ,
2115+ headers,
2116+ qs : {
2117+ where : JSON . stringify ( {
2118+ [ maliciousField ] : { $exists : true } ,
2119+ } ) ,
2120+ } ,
2121+ } ) . catch ( e => e ) ;
2122+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2123+ } ) ;
2124+
2125+ describe ( 'validateQuery key name enforcement' , ( ) => {
2126+ const maliciousField = 'field"; DROP TABLE test --' ;
2127+ const noMasterHeaders = {
2128+ 'Content-Type' : 'application/json' ,
2129+ 'X-Parse-Application-Id' : 'test' ,
2130+ 'X-Parse-REST-API-Key' : 'rest' ,
2131+ } ;
2132+
2133+ it ( 'rejects malicious field name in find without master key' , async ( ) => {
2134+ const response = await request ( {
2135+ method : 'GET' ,
2136+ url : `${ serverURL } /classes/TestClass` ,
2137+ headers : noMasterHeaders ,
2138+ qs : {
2139+ where : JSON . stringify ( { [ maliciousField ] : 'value' } ) ,
2140+ } ,
2141+ } ) . catch ( e => e ) ;
2142+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2143+ } ) ;
2144+
2145+ it ( 'rejects malicious field name in find with master key' , async ( ) => {
2146+ const response = await request ( {
2147+ method : 'GET' ,
2148+ url : `${ serverURL } /classes/TestClass` ,
2149+ headers,
2150+ qs : {
2151+ where : JSON . stringify ( { [ maliciousField ] : 'value' } ) ,
2152+ } ,
2153+ } ) . catch ( e => e ) ;
2154+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2155+ } ) ;
2156+
2157+ it ( 'allows master key to query whitelisted internal field _email_verify_token' , async ( ) => {
2158+ await reconfigureServer ( {
2159+ verifyUserEmails : true ,
2160+ emailAdapter : {
2161+ sendVerificationEmail : ( ) => Promise . resolve ( ) ,
2162+ sendPasswordResetEmail : ( ) => Promise . resolve ( ) ,
2163+ sendMail : ( ) => { } ,
2164+ } ,
2165+ appName : 'test' ,
2166+ publicServerURL : 'http://localhost:8378/1' ,
2167+ } ) ;
2168+ const user = new Parse . User ( ) ;
2169+ user . setUsername ( 'testuser' ) ;
2170+ user . setPassword ( 'testpass' ) ;
2171+ user . setEmail ( 'test@example.com' ) ;
2172+ await user . signUp ( ) ;
2173+ const response = await request ( {
2174+ method : 'GET' ,
2175+ url : `${ serverURL } /classes/_User` ,
2176+ headers,
2177+ qs : {
2178+ where : JSON . stringify ( { _email_verify_token : { $exists : true } } ) ,
2179+ } ,
2180+ } ) ;
2181+ expect ( response . data . results . length ) . toBeGreaterThan ( 0 ) ;
2182+ } ) ;
2183+
2184+ it ( 'rejects non-master key querying internal field _email_verify_token' , async ( ) => {
2185+ const response = await request ( {
2186+ method : 'GET' ,
2187+ url : `${ serverURL } /classes/_User` ,
2188+ headers : noMasterHeaders ,
2189+ qs : {
2190+ where : JSON . stringify ( { _email_verify_token : { $exists : true } } ) ,
2191+ } ,
2192+ } ) . catch ( e => e ) ;
2193+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2194+ } ) ;
2195+
2196+ describe ( 'non-master key cannot update internal fields' , ( ) => {
2197+ const internalFields = [
2198+ '_rperm' ,
2199+ '_wperm' ,
2200+ '_hashed_password' ,
2201+ '_email_verify_token' ,
2202+ '_perishable_token' ,
2203+ '_perishable_token_expires_at' ,
2204+ '_email_verify_token_expires_at' ,
2205+ '_failed_login_count' ,
2206+ '_account_lockout_expires_at' ,
2207+ '_password_changed_at' ,
2208+ '_password_history' ,
2209+ '_tombstone' ,
2210+ '_session_token' ,
2211+ ] ;
2212+
2213+ for ( const field of internalFields ) {
2214+ it ( `rejects non-master key updating ${ field } ` , async ( ) => {
2215+ const user = new Parse . User ( ) ;
2216+ user . setUsername ( `updatetest_${ field } ` ) ;
2217+ user . setPassword ( 'password123' ) ;
2218+ await user . signUp ( ) ;
2219+ const response = await request ( {
2220+ method : 'PUT' ,
2221+ url : `${ serverURL } /classes/_User/${ user . id } ` ,
2222+ headers : {
2223+ 'Content-Type' : 'application/json' ,
2224+ 'X-Parse-Application-Id' : 'test' ,
2225+ 'X-Parse-REST-API-Key' : 'rest' ,
2226+ 'X-Parse-Session-Token' : user . getSessionToken ( ) ,
2227+ } ,
2228+ body : JSON . stringify ( { [ field ] : 'malicious_value' } ) ,
2229+ } ) . catch ( e => e ) ;
2230+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2231+ } ) ;
2232+ }
2233+ } ) ;
2234+ } ) ;
2235+ } ) ;
0 commit comments