Skip to content

Commit fb69186

Browse files
committed
fix
1 parent 729362a commit fb69186

File tree

5 files changed

+201
-1
lines changed

5 files changed

+201
-1
lines changed

spec/MongoTransform.spec.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,4 +692,52 @@ describe('relativeTimeToDate', () => {
692692
});
693693
});
694694
});
695+
696+
describe('token field injection prevention', () => {
697+
it('should reject non-string value for _perishable_token', () => {
698+
expect(() => {
699+
transform.transformWhere(null, { _perishable_token: { $regex: '^a' } });
700+
}).toThrow();
701+
});
702+
703+
it('should reject $ne operator for _perishable_token', () => {
704+
expect(() => {
705+
transform.transformWhere(null, { _perishable_token: { $ne: null } });
706+
}).toThrow();
707+
});
708+
709+
it('should reject $exists operator for _perishable_token', () => {
710+
expect(() => {
711+
transform.transformWhere(null, { _perishable_token: { $exists: true } });
712+
}).toThrow();
713+
});
714+
715+
it('should reject non-string value for _email_verify_token', () => {
716+
expect(() => {
717+
transform.transformWhere(null, { _email_verify_token: { $regex: '^a' } });
718+
}).toThrow();
719+
});
720+
721+
it('should reject $ne operator for _email_verify_token', () => {
722+
expect(() => {
723+
transform.transformWhere(null, { _email_verify_token: { $ne: null } });
724+
}).toThrow();
725+
});
726+
727+
it('should reject $exists operator for _email_verify_token', () => {
728+
expect(() => {
729+
transform.transformWhere(null, { _email_verify_token: { $exists: true } });
730+
}).toThrow();
731+
});
732+
733+
it('should allow string value for _perishable_token', () => {
734+
const result = transform.transformWhere(null, { _perishable_token: 'validtoken123' });
735+
expect(result._perishable_token).toEqual('validtoken123');
736+
});
737+
738+
it('should allow string value for _email_verify_token', () => {
739+
const result = transform.transformWhere(null, { _email_verify_token: 'validtoken123' });
740+
expect(result._email_verify_token).toEqual('validtoken123');
741+
});
742+
});
695743
});

spec/RegexVulnerabilities.spec.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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');

src/Adapters/Storage/Mongo/MongoTransform.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,12 @@ function transformQueryKeyValue(className, key, value, schema, count = false) {
284284
break;
285285
case '_rperm':
286286
case '_wperm':
287+
return { key, value };
287288
case '_perishable_token':
288289
case '_email_verify_token':
290+
if (typeof value !== 'string') {
291+
throw new Parse.Error(Parse.Error.INVALID_VALUE, `${key} must be a string`);
292+
}
289293
return { key, value };
290294
case '$or':
291295
case '$and':

src/Routers/PagesRouter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ export class PagesRouter extends PromiseRouter {
108108
resendVerificationEmail(req) {
109109
const config = req.config;
110110
const username = req.body?.username;
111-
const token = req.body?.token;
111+
const rawToken = req.body?.token;
112+
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
112113

113114
if (!config) {
114115
this.invalidRequest();

src/Routers/UsersRouter.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,10 @@ export class UsersRouter extends ClassesRouter {
458458
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
459459
}
460460

461+
if (token && typeof token !== 'string') {
462+
throw new Parse.Error(Parse.Error.INVALID_VALUE, 'token must be a string');
463+
}
464+
461465
let userResults = null;
462466
let userData = null;
463467

0 commit comments

Comments
 (0)