@@ -2112,3 +2112,258 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint
21122112 }
21132113 } ) ;
21142114} ) ;
2115+
2116+ describe ( '(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field name in PostgreSQL adapter' , ( ) => {
2117+ const headers = {
2118+ 'Content-Type' : 'application/json' ,
2119+ 'X-Parse-Application-Id' : 'test' ,
2120+ 'X-Parse-REST-API-Key' : 'rest' ,
2121+ 'X-Parse-Master-Key' : 'test' ,
2122+ } ;
2123+ const serverURL = 'http://localhost:8378/1' ;
2124+
2125+ beforeEach ( async ( ) => {
2126+ const obj = new Parse . Object ( 'TestClass' ) ;
2127+ obj . set ( 'playerName' , 'Alice' ) ;
2128+ obj . set ( 'score' , 100 ) ;
2129+ await obj . save ( null , { useMasterKey : true } ) ;
2130+ } ) ;
2131+
2132+ it ( 'rejects field names containing double quotes in $regex query with master key' , async ( ) => {
2133+ const maliciousField = 'playerName" OR 1=1 --' ;
2134+ const response = await request ( {
2135+ method : 'GET' ,
2136+ url : `${ serverURL } /classes/TestClass` ,
2137+ headers,
2138+ qs : {
2139+ where : JSON . stringify ( {
2140+ [ maliciousField ] : { $regex : 'x' } ,
2141+ } ) ,
2142+ } ,
2143+ } ) . catch ( e => e ) ;
2144+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2145+ } ) ;
2146+
2147+ it ( 'rejects field names containing single quotes in $regex query with master key' , async ( ) => {
2148+ const maliciousField = "playerName' OR '1'='1" ;
2149+ const response = await request ( {
2150+ method : 'GET' ,
2151+ url : `${ serverURL } /classes/TestClass` ,
2152+ headers,
2153+ qs : {
2154+ where : JSON . stringify ( {
2155+ [ maliciousField ] : { $regex : 'x' } ,
2156+ } ) ,
2157+ } ,
2158+ } ) . catch ( e => e ) ;
2159+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2160+ } ) ;
2161+
2162+ it ( 'rejects field names containing semicolons in $regex query with master key' , async ( ) => {
2163+ const maliciousField = 'playerName; DROP TABLE "TestClass" --' ;
2164+ const response = await request ( {
2165+ method : 'GET' ,
2166+ url : `${ serverURL } /classes/TestClass` ,
2167+ headers,
2168+ qs : {
2169+ where : JSON . stringify ( {
2170+ [ maliciousField ] : { $regex : 'x' } ,
2171+ } ) ,
2172+ } ,
2173+ } ) . catch ( e => e ) ;
2174+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2175+ } ) ;
2176+
2177+ it ( 'rejects field names containing parentheses in $regex query with master key' , async ( ) => {
2178+ const maliciousField = 'playerName" ~ \'x\' OR (SELECT 1) --' ;
2179+ const response = await request ( {
2180+ method : 'GET' ,
2181+ url : `${ serverURL } /classes/TestClass` ,
2182+ headers,
2183+ qs : {
2184+ where : JSON . stringify ( {
2185+ [ maliciousField ] : { $regex : 'x' } ,
2186+ } ) ,
2187+ } ,
2188+ } ) . catch ( e => e ) ;
2189+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2190+ } ) ;
2191+
2192+ it ( 'allows legitimate $regex query with master key' , async ( ) => {
2193+ const response = await request ( {
2194+ method : 'GET' ,
2195+ url : `${ serverURL } /classes/TestClass` ,
2196+ headers,
2197+ qs : {
2198+ where : JSON . stringify ( {
2199+ playerName : { $regex : 'Ali' } ,
2200+ } ) ,
2201+ } ,
2202+ } ) ;
2203+ expect ( response . data . results . length ) . toBe ( 1 ) ;
2204+ expect ( response . data . results [ 0 ] . playerName ) . toBe ( 'Alice' ) ;
2205+ } ) ;
2206+
2207+ it ( 'allows legitimate $regex query with dot notation and master key' , async ( ) => {
2208+ const obj = new Parse . Object ( 'TestClass' ) ;
2209+ obj . set ( 'metadata' , { tag : 'hello-world' } ) ;
2210+ await obj . save ( null , { useMasterKey : true } ) ;
2211+ const response = await request ( {
2212+ method : 'GET' ,
2213+ url : `${ serverURL } /classes/TestClass` ,
2214+ headers,
2215+ qs : {
2216+ where : JSON . stringify ( {
2217+ 'metadata.tag' : { $regex : 'hello' } ,
2218+ } ) ,
2219+ } ,
2220+ } ) ;
2221+ expect ( response . data . results . length ) . toBe ( 1 ) ;
2222+ expect ( response . data . results [ 0 ] . metadata . tag ) . toBe ( 'hello-world' ) ;
2223+ } ) ;
2224+
2225+ it ( 'allows legitimate $regex query without master key' , async ( ) => {
2226+ const response = await request ( {
2227+ method : 'GET' ,
2228+ url : `${ serverURL } /classes/TestClass` ,
2229+ headers : {
2230+ 'Content-Type' : 'application/json' ,
2231+ 'X-Parse-Application-Id' : 'test' ,
2232+ 'X-Parse-REST-API-Key' : 'rest' ,
2233+ } ,
2234+ qs : {
2235+ where : JSON . stringify ( {
2236+ playerName : { $regex : 'Ali' } ,
2237+ } ) ,
2238+ } ,
2239+ } ) ;
2240+ expect ( response . data . results . length ) . toBe ( 1 ) ;
2241+ expect ( response . data . results [ 0 ] . playerName ) . toBe ( 'Alice' ) ;
2242+ } ) ;
2243+
2244+ it ( 'rejects field names with SQL injection via non-$regex operators with master key' , async ( ) => {
2245+ const maliciousField = 'playerName" OR 1=1 --' ;
2246+ const response = await request ( {
2247+ method : 'GET' ,
2248+ url : `${ serverURL } /classes/TestClass` ,
2249+ headers,
2250+ qs : {
2251+ where : JSON . stringify ( {
2252+ [ maliciousField ] : { $exists : true } ,
2253+ } ) ,
2254+ } ,
2255+ } ) . catch ( e => e ) ;
2256+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2257+ } ) ;
2258+
2259+ describe ( 'validateQuery key name enforcement' , ( ) => {
2260+ const maliciousField = 'field"; DROP TABLE test --' ;
2261+ const noMasterHeaders = {
2262+ 'Content-Type' : 'application/json' ,
2263+ 'X-Parse-Application-Id' : 'test' ,
2264+ 'X-Parse-REST-API-Key' : 'rest' ,
2265+ } ;
2266+
2267+ it ( 'rejects malicious field name in find without master key' , async ( ) => {
2268+ const response = await request ( {
2269+ method : 'GET' ,
2270+ url : `${ serverURL } /classes/TestClass` ,
2271+ headers : noMasterHeaders ,
2272+ qs : {
2273+ where : JSON . stringify ( { [ maliciousField ] : 'value' } ) ,
2274+ } ,
2275+ } ) . catch ( e => e ) ;
2276+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2277+ } ) ;
2278+
2279+ it ( 'rejects malicious field name in find with master key' , async ( ) => {
2280+ const response = await request ( {
2281+ method : 'GET' ,
2282+ url : `${ serverURL } /classes/TestClass` ,
2283+ headers,
2284+ qs : {
2285+ where : JSON . stringify ( { [ maliciousField ] : 'value' } ) ,
2286+ } ,
2287+ } ) . catch ( e => e ) ;
2288+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2289+ } ) ;
2290+
2291+ it ( 'allows master key to query whitelisted internal field _email_verify_token' , async ( ) => {
2292+ await reconfigureServer ( {
2293+ verifyUserEmails : true ,
2294+ emailAdapter : {
2295+ sendVerificationEmail : ( ) => Promise . resolve ( ) ,
2296+ sendPasswordResetEmail : ( ) => Promise . resolve ( ) ,
2297+ sendMail : ( ) => { } ,
2298+ } ,
2299+ appName : 'test' ,
2300+ publicServerURL : 'http://localhost:8378/1' ,
2301+ } ) ;
2302+ const user = new Parse . User ( ) ;
2303+ user . setUsername ( 'testuser' ) ;
2304+ user . setPassword ( 'testpass' ) ;
2305+ user . setEmail ( 'test@example.com' ) ;
2306+ await user . signUp ( ) ;
2307+ const response = await request ( {
2308+ method : 'GET' ,
2309+ url : `${ serverURL } /classes/_User` ,
2310+ headers,
2311+ qs : {
2312+ where : JSON . stringify ( { _email_verify_token : { $exists : true } } ) ,
2313+ } ,
2314+ } ) ;
2315+ expect ( response . data . results . length ) . toBeGreaterThan ( 0 ) ;
2316+ } ) ;
2317+
2318+ it ( 'rejects non-master key querying internal field _email_verify_token' , async ( ) => {
2319+ const response = await request ( {
2320+ method : 'GET' ,
2321+ url : `${ serverURL } /classes/_User` ,
2322+ headers : noMasterHeaders ,
2323+ qs : {
2324+ where : JSON . stringify ( { _email_verify_token : { $exists : true } } ) ,
2325+ } ,
2326+ } ) . catch ( e => e ) ;
2327+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2328+ } ) ;
2329+
2330+ describe ( 'non-master key cannot update internal fields' , ( ) => {
2331+ const internalFields = [
2332+ '_rperm' ,
2333+ '_wperm' ,
2334+ '_hashed_password' ,
2335+ '_email_verify_token' ,
2336+ '_perishable_token' ,
2337+ '_perishable_token_expires_at' ,
2338+ '_email_verify_token_expires_at' ,
2339+ '_failed_login_count' ,
2340+ '_account_lockout_expires_at' ,
2341+ '_password_changed_at' ,
2342+ '_password_history' ,
2343+ '_tombstone' ,
2344+ '_session_token' ,
2345+ ] ;
2346+
2347+ for ( const field of internalFields ) {
2348+ it ( `rejects non-master key updating ${ field } ` , async ( ) => {
2349+ const user = new Parse . User ( ) ;
2350+ user . setUsername ( `updatetest_${ field } ` ) ;
2351+ user . setPassword ( 'password123' ) ;
2352+ await user . signUp ( ) ;
2353+ const response = await request ( {
2354+ method : 'PUT' ,
2355+ url : `${ serverURL } /classes/_User/${ user . id } ` ,
2356+ headers : {
2357+ 'Content-Type' : 'application/json' ,
2358+ 'X-Parse-Application-Id' : 'test' ,
2359+ 'X-Parse-REST-API-Key' : 'rest' ,
2360+ 'X-Parse-Session-Token' : user . getSessionToken ( ) ,
2361+ } ,
2362+ body : JSON . stringify ( { [ field ] : 'malicious_value' } ) ,
2363+ } ) . catch ( e => e ) ;
2364+ expect ( response . data . code ) . toBe ( Parse . Error . INVALID_KEY_NAME ) ;
2365+ } ) ;
2366+ }
2367+ } ) ;
2368+ } ) ;
2369+ } ) ;
0 commit comments