@@ -4040,6 +4040,91 @@ describe('(GHSA-2299-ghjr-6vjp) MFA recovery code reuse via concurrent requests'
40404040 } ) ;
40414041} ) ;
40424042
4043+ describe ( '(GHSA-w73w-g5xw-rwhf) MFA recovery code reuse via concurrent authData-only login' , ( ) => {
4044+ const mfaHeaders = {
4045+ 'X-Parse-Application-Id' : 'test' ,
4046+ 'X-Parse-REST-API-Key' : 'rest' ,
4047+ 'Content-Type' : 'application/json' ,
4048+ } ;
4049+
4050+ let fakeProvider ;
4051+
4052+ beforeEach ( async ( ) => {
4053+ fakeProvider = {
4054+ validateAppId : ( ) => Promise . resolve ( ) ,
4055+ validateAuthData : ( ) => Promise . resolve ( ) ,
4056+ } ;
4057+ await reconfigureServer ( {
4058+ auth : {
4059+ fakeProvider,
4060+ mfa : {
4061+ enabled : true ,
4062+ options : [ 'TOTP' ] ,
4063+ algorithm : 'SHA1' ,
4064+ digits : 6 ,
4065+ period : 30 ,
4066+ } ,
4067+ } ,
4068+ } ) ;
4069+ } ) ;
4070+
4071+ it ( 'rejects concurrent authData-only logins using the same MFA recovery code' , async ( ) => {
4072+ const OTPAuth = require ( 'otpauth' ) ;
4073+
4074+ // Create user via authData login with fake provider
4075+ const user = await Parse . User . logInWith ( 'fakeProvider' , {
4076+ authData : { id : 'user1' , token : 'fakeToken' } ,
4077+ } ) ;
4078+
4079+ // Enable MFA for this user
4080+ const secret = new OTPAuth . Secret ( ) ;
4081+ const totp = new OTPAuth . TOTP ( {
4082+ algorithm : 'SHA1' ,
4083+ digits : 6 ,
4084+ period : 30 ,
4085+ secret,
4086+ } ) ;
4087+ const token = totp . generate ( ) ;
4088+ await user . save (
4089+ { authData : { mfa : { secret : secret . base32 , token } } } ,
4090+ { sessionToken : user . getSessionToken ( ) }
4091+ ) ;
4092+
4093+ // Get recovery codes from stored auth data
4094+ await user . fetch ( { useMasterKey : true } ) ;
4095+ const recoveryCode = user . get ( 'authData' ) . mfa . recovery [ 0 ] ;
4096+ expect ( recoveryCode ) . toBeDefined ( ) ;
4097+
4098+ // Send concurrent authData-only login requests with the same recovery code
4099+ const loginWithRecovery = ( ) =>
4100+ request ( {
4101+ method : 'POST' ,
4102+ url : 'http://localhost:8378/1/users' ,
4103+ headers : mfaHeaders ,
4104+ body : JSON . stringify ( {
4105+ authData : {
4106+ fakeProvider : { id : 'user1' , token : 'fakeToken' } ,
4107+ mfa : { token : recoveryCode } ,
4108+ } ,
4109+ } ) ,
4110+ } ) ;
4111+
4112+ const results = await Promise . allSettled ( Array ( 10 ) . fill ( ) . map ( ( ) => loginWithRecovery ( ) ) ) ;
4113+
4114+ const succeeded = results . filter ( r => r . status === 'fulfilled' ) ;
4115+ const failed = results . filter ( r => r . status === 'rejected' ) ;
4116+
4117+ // Exactly one request should succeed; all others should fail
4118+ expect ( succeeded . length ) . toBe ( 1 ) ;
4119+ expect ( failed . length ) . toBe ( 9 ) ;
4120+
4121+ // Verify the recovery code has been consumed
4122+ await user . fetch ( { useMasterKey : true } ) ;
4123+ const remainingRecovery = user . get ( 'authData' ) . mfa . recovery ;
4124+ expect ( remainingRecovery ) . not . toContain ( recoveryCode ) ;
4125+ } ) ;
4126+ } ) ;
4127+
40434128describe ( '(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context' , ( ) => {
40444129 const headers = {
40454130 'X-Parse-Application-Id' : 'test' ,
0 commit comments