@@ -125,6 +125,149 @@ describe('Regex Vulnerabilities', () => {
125125 } ) ;
126126 } ) ;
127127
128+ describe ( 'on password reset request via token (handleResetRequest)' , ( ) => {
129+ beforeEach ( async ( ) => {
130+ user = await Parse . User . logIn ( 'someemail@somedomain.com' , 'somepassword' ) ;
131+ // Trigger a password reset to generate a _perishable_token
132+ await request ( {
133+ url : `${ serverURL } /requestPasswordReset` ,
134+ method : 'POST' ,
135+ headers,
136+ body : JSON . stringify ( {
137+ ...keys ,
138+ _method : 'POST' ,
139+ email : 'someemail@somedomain.com' ,
140+ } ) ,
141+ } ) ;
142+ // Expire the token so the handleResetRequest token-lookup branch matches
143+ await Parse . Server . database . update (
144+ '_User' ,
145+ { objectId : user . id } ,
146+ {
147+ _perishable_token_expires_at : new Date ( Date . now ( ) - 10000 ) ,
148+ }
149+ ) ;
150+ } ) ;
151+
152+ it ( 'should not allow $ne operator to match user via token injection' , async ( ) => {
153+ // Without the fix, {$ne: null} matches any user with a non-null expired token,
154+ // causing a password reset email to be sent — a boolean oracle for token extraction.
155+ try {
156+ await request ( {
157+ url : `${ serverURL } /requestPasswordReset` ,
158+ method : 'POST' ,
159+ headers,
160+ body : JSON . stringify ( {
161+ ...keys ,
162+ token : { $ne : null } ,
163+ } ) ,
164+ } ) ;
165+ fail ( 'should not succeed with $ne token' ) ;
166+ } catch ( e ) {
167+ expect ( e . data . code ) . toEqual ( Parse . Error . INVALID_VALUE ) ;
168+ }
169+ } ) ;
170+
171+ it ( 'should not allow $regex operator to extract token via injection' , async ( ) => {
172+ try {
173+ await request ( {
174+ url : `${ serverURL } /requestPasswordReset` ,
175+ method : 'POST' ,
176+ headers,
177+ body : JSON . stringify ( {
178+ ...keys ,
179+ token : { $regex : '^.' } ,
180+ } ) ,
181+ } ) ;
182+ fail ( 'should not succeed with $regex token' ) ;
183+ } catch ( e ) {
184+ expect ( e . data . code ) . toEqual ( Parse . Error . INVALID_VALUE ) ;
185+ }
186+ } ) ;
187+
188+ it ( 'should not allow $exists operator for token injection' , async ( ) => {
189+ try {
190+ await request ( {
191+ url : `${ serverURL } /requestPasswordReset` ,
192+ method : 'POST' ,
193+ headers,
194+ body : JSON . stringify ( {
195+ ...keys ,
196+ token : { $exists : true } ,
197+ } ) ,
198+ } ) ;
199+ fail ( 'should not succeed with $exists token' ) ;
200+ } catch ( e ) {
201+ expect ( e . data . code ) . toEqual ( Parse . Error . INVALID_VALUE ) ;
202+ }
203+ } ) ;
204+
205+ it ( 'should not allow $gt operator for token injection' , async ( ) => {
206+ try {
207+ await request ( {
208+ url : `${ serverURL } /requestPasswordReset` ,
209+ method : 'POST' ,
210+ headers,
211+ body : JSON . stringify ( {
212+ ...keys ,
213+ token : { $gt : '' } ,
214+ } ) ,
215+ } ) ;
216+ fail ( 'should not succeed with $gt token' ) ;
217+ } catch ( e ) {
218+ expect ( e . data . code ) . toEqual ( Parse . Error . INVALID_VALUE ) ;
219+ }
220+ } ) ;
221+ } ) ;
222+
223+ describe ( 'on resend verification email' , ( ) => {
224+ // The PagesRouter uses express.urlencoded({ extended: false }) which does not parse
225+ // nested objects (e.g. token[$regex]=^.), so the HTTP layer already blocks object injection.
226+ // The toString() guard in resendVerificationEmail() is defense-in-depth in case the
227+ // body parser configuration changes. These tests verify the guard works correctly
228+ // by directly testing the PagesRouter method.
229+ it ( 'should sanitize non-string token to string via toString()' , async ( ) => {
230+ const { PagesRouter } = require ( '../lib/Routers/PagesRouter' ) ;
231+ const router = new PagesRouter ( ) ;
232+ const goToPage = spyOn ( router , 'goToPage' ) . and . returnValue ( Promise . resolve ( ) ) ;
233+ const resendSpy = jasmine . createSpy ( 'resendVerificationEmail' ) . and . returnValue ( Promise . resolve ( ) ) ;
234+ const req = {
235+ config : {
236+ userController : { resendVerificationEmail : resendSpy } ,
237+ } ,
238+ body : {
239+ username : 'testuser' ,
240+ token : { $regex : '^.' } ,
241+ } ,
242+ } ;
243+ await router . resendVerificationEmail ( req ) ;
244+ // The token passed to userController.resendVerificationEmail should be a string
245+ const passedToken = resendSpy . calls . first ( ) . args [ 2 ] ;
246+ expect ( typeof passedToken ) . toEqual ( 'string' ) ;
247+ expect ( passedToken ) . toEqual ( '[object Object]' ) ;
248+ } ) ;
249+
250+ it ( 'should pass through valid string token unchanged' , async ( ) => {
251+ const { PagesRouter } = require ( '../lib/Routers/PagesRouter' ) ;
252+ const router = new PagesRouter ( ) ;
253+ const goToPage = spyOn ( router , 'goToPage' ) . and . returnValue ( Promise . resolve ( ) ) ;
254+ const resendSpy = jasmine . createSpy ( 'resendVerificationEmail' ) . and . returnValue ( Promise . resolve ( ) ) ;
255+ const req = {
256+ config : {
257+ userController : { resendVerificationEmail : resendSpy } ,
258+ } ,
259+ body : {
260+ username : 'testuser' ,
261+ token : 'validtoken123' ,
262+ } ,
263+ } ;
264+ await router . resendVerificationEmail ( req ) ;
265+ const passedToken = resendSpy . calls . first ( ) . args [ 2 ] ;
266+ expect ( typeof passedToken ) . toEqual ( 'string' ) ;
267+ expect ( passedToken ) . toEqual ( 'validtoken123' ) ;
268+ } ) ;
269+ } ) ;
270+
128271 describe ( 'on password reset' , ( ) => {
129272 beforeEach ( async ( ) => {
130273 user = await Parse . User . logIn ( 'someemail@somedomain.com' , 'somepassword' ) ;
0 commit comments