@@ -4315,6 +4315,91 @@ describe('(GHSA-2299-ghjr-6vjp) MFA recovery code reuse via concurrent requests'
43154315 } ) ;
43164316} ) ;
43174317
4318+ describe ( '(GHSA-w73w-g5xw-rwhf) MFA recovery code reuse via concurrent authData-only login' , ( ) => {
4319+ const mfaHeaders = {
4320+ 'X-Parse-Application-Id' : 'test' ,
4321+ 'X-Parse-REST-API-Key' : 'rest' ,
4322+ 'Content-Type' : 'application/json' ,
4323+ } ;
4324+
4325+ let fakeProvider ;
4326+
4327+ beforeEach ( async ( ) => {
4328+ fakeProvider = {
4329+ validateAppId : ( ) => Promise . resolve ( ) ,
4330+ validateAuthData : ( ) => Promise . resolve ( ) ,
4331+ } ;
4332+ await reconfigureServer ( {
4333+ auth : {
4334+ fakeProvider,
4335+ mfa : {
4336+ enabled : true ,
4337+ options : [ 'TOTP' ] ,
4338+ algorithm : 'SHA1' ,
4339+ digits : 6 ,
4340+ period : 30 ,
4341+ } ,
4342+ } ,
4343+ } ) ;
4344+ } ) ;
4345+
4346+ it ( 'rejects concurrent authData-only logins using the same MFA recovery code' , async ( ) => {
4347+ const OTPAuth = require ( 'otpauth' ) ;
4348+
4349+ // Create user via authData login with fake provider
4350+ const user = await Parse . User . logInWith ( 'fakeProvider' , {
4351+ authData : { id : 'user1' , token : 'fakeToken' } ,
4352+ } ) ;
4353+
4354+ // Enable MFA for this user
4355+ const secret = new OTPAuth . Secret ( ) ;
4356+ const totp = new OTPAuth . TOTP ( {
4357+ algorithm : 'SHA1' ,
4358+ digits : 6 ,
4359+ period : 30 ,
4360+ secret,
4361+ } ) ;
4362+ const token = totp . generate ( ) ;
4363+ await user . save (
4364+ { authData : { mfa : { secret : secret . base32 , token } } } ,
4365+ { sessionToken : user . getSessionToken ( ) }
4366+ ) ;
4367+
4368+ // Get recovery codes from stored auth data
4369+ await user . fetch ( { useMasterKey : true } ) ;
4370+ const recoveryCode = user . get ( 'authData' ) . mfa . recovery [ 0 ] ;
4371+ expect ( recoveryCode ) . toBeDefined ( ) ;
4372+
4373+ // Send concurrent authData-only login requests with the same recovery code
4374+ const loginWithRecovery = ( ) =>
4375+ request ( {
4376+ method : 'POST' ,
4377+ url : 'http://localhost:8378/1/users' ,
4378+ headers : mfaHeaders ,
4379+ body : JSON . stringify ( {
4380+ authData : {
4381+ fakeProvider : { id : 'user1' , token : 'fakeToken' } ,
4382+ mfa : { token : recoveryCode } ,
4383+ } ,
4384+ } ) ,
4385+ } ) ;
4386+
4387+ const results = await Promise . allSettled ( Array ( 10 ) . fill ( ) . map ( ( ) => loginWithRecovery ( ) ) ) ;
4388+
4389+ const succeeded = results . filter ( r => r . status === 'fulfilled' ) ;
4390+ const failed = results . filter ( r => r . status === 'rejected' ) ;
4391+
4392+ // Exactly one request should succeed; all others should fail
4393+ expect ( succeeded . length ) . toBe ( 1 ) ;
4394+ expect ( failed . length ) . toBe ( 9 ) ;
4395+
4396+ // Verify the recovery code has been consumed
4397+ await user . fetch ( { useMasterKey : true } ) ;
4398+ const remainingRecovery = user . get ( 'authData' ) . mfa . recovery ;
4399+ expect ( remainingRecovery ) . not . toContain ( recoveryCode ) ;
4400+ } ) ;
4401+ } ) ;
4402+
43184403describe ( '(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field names in PostgreSQL adapter' , ( ) => {
43194404 const headers = {
43204405 'Content-Type' : 'application/json' ,
0 commit comments