mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: NoSQL injection via token type in password reset and email verification endpoints ([GHSA-vgjh-hmwf-c588](https://github.com/parse-community/parse-server/security/advisories/GHSA-vgjh-hmwf-c588)) (#10129)
This commit is contained in:
@@ -692,4 +692,5 @@ describe('relativeTimeToDate', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -125,6 +125,153 @@ describe('Regex Vulnerabilities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('on password reset request via token (handleResetRequest)', () => {
|
||||
beforeEach(async () => {
|
||||
user = await Parse.User.logIn('someemail@somedomain.com', 'somepassword');
|
||||
// Trigger a password reset to generate a _perishable_token
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
_method: 'POST',
|
||||
email: 'someemail@somedomain.com',
|
||||
}),
|
||||
});
|
||||
// Expire the token so the handleResetRequest token-lookup branch matches
|
||||
await Parse.Server.database.update(
|
||||
'_User',
|
||||
{ objectId: user.id },
|
||||
{
|
||||
_perishable_token_expires_at: new Date(Date.now() - 10000),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow $ne operator to match user via token injection', async () => {
|
||||
// Without the fix, {$ne: null} matches any user with a non-null expired token,
|
||||
// causing a password reset email to be sent — a boolean oracle for token extraction.
|
||||
try {
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
token: { $ne: null },
|
||||
}),
|
||||
});
|
||||
fail('should not succeed with $ne token');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not allow $regex operator to extract token via injection', async () => {
|
||||
try {
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
token: { $regex: '^.' },
|
||||
}),
|
||||
});
|
||||
fail('should not succeed with $regex token');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not allow $exists operator for token injection', async () => {
|
||||
try {
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
token: { $exists: true },
|
||||
}),
|
||||
});
|
||||
fail('should not succeed with $exists token');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not allow $gt operator for token injection', async () => {
|
||||
try {
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
token: { $gt: '' },
|
||||
}),
|
||||
});
|
||||
fail('should not succeed with $gt token');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('on resend verification email', () => {
|
||||
// The PagesRouter uses express.urlencoded({ extended: false }) which does not parse
|
||||
// nested objects (e.g. token[$regex]=^.), so the HTTP layer already blocks object injection.
|
||||
// The toString() guard in resendVerificationEmail() is defense-in-depth in case the
|
||||
// body parser configuration changes. These tests verify the guard works correctly
|
||||
// by directly testing the PagesRouter method.
|
||||
it('should sanitize non-string token to string via toString()', async () => {
|
||||
const { PagesRouter } = require('../lib/Routers/PagesRouter');
|
||||
const router = new PagesRouter();
|
||||
spyOn(router, 'goToPage').and.returnValue(Promise.resolve());
|
||||
const resendSpy = jasmine
|
||||
.createSpy('resendVerificationEmail')
|
||||
.and.returnValue(Promise.resolve());
|
||||
const req = {
|
||||
config: {
|
||||
userController: { resendVerificationEmail: resendSpy },
|
||||
},
|
||||
body: {
|
||||
username: 'testuser',
|
||||
token: { $regex: '^.' },
|
||||
},
|
||||
};
|
||||
await router.resendVerificationEmail(req);
|
||||
// The token passed to userController.resendVerificationEmail should be a string
|
||||
const passedToken = resendSpy.calls.first().args[2];
|
||||
expect(typeof passedToken).toEqual('string');
|
||||
expect(passedToken).toEqual('[object Object]');
|
||||
});
|
||||
|
||||
it('should pass through valid string token unchanged', async () => {
|
||||
const { PagesRouter } = require('../lib/Routers/PagesRouter');
|
||||
const router = new PagesRouter();
|
||||
spyOn(router, 'goToPage').and.returnValue(Promise.resolve());
|
||||
const resendSpy = jasmine
|
||||
.createSpy('resendVerificationEmail')
|
||||
.and.returnValue(Promise.resolve());
|
||||
const req = {
|
||||
config: {
|
||||
userController: { resendVerificationEmail: resendSpy },
|
||||
},
|
||||
body: {
|
||||
username: 'testuser',
|
||||
token: 'validtoken123',
|
||||
},
|
||||
};
|
||||
await router.resendVerificationEmail(req);
|
||||
const passedToken = resendSpy.calls.first().args[2];
|
||||
expect(typeof passedToken).toEqual('string');
|
||||
expect(passedToken).toEqual('validtoken123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on password reset', () => {
|
||||
beforeEach(async () => {
|
||||
user = await Parse.User.logIn('someemail@somedomain.com', 'somepassword');
|
||||
|
||||
@@ -108,7 +108,8 @@ export class PagesRouter extends PromiseRouter {
|
||||
resendVerificationEmail(req) {
|
||||
const config = req.config;
|
||||
const username = req.body?.username;
|
||||
const token = req.body?.token;
|
||||
const rawToken = req.body?.token;
|
||||
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
|
||||
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
|
||||
@@ -458,6 +458,10 @@ export class UsersRouter extends ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
|
||||
}
|
||||
|
||||
if (token && typeof token !== 'string') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_VALUE, 'token must be a string');
|
||||
}
|
||||
|
||||
let userResults = null;
|
||||
let userData = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user