Skip to content

Commit 6c9021b

Browse files
committed
fix
1 parent f0f3bbb commit 6c9021b

File tree

7 files changed

+211
-0
lines changed

7 files changed

+211
-0
lines changed

spec/vulnerabilities.spec.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const request = require('../lib/request');
2+
const Config = require('../lib/Config');
23

34
describe('Vulnerabilities', () => {
45
describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => {
@@ -1704,3 +1705,183 @@ describe('(GHSA-r2m8-pxm9-9c4g) Protected fields WHERE clause bypass via dot-not
17041705
expect(res.status).toBe(400);
17051706
});
17061707
});
1708+
1709+
describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint', () => {
1710+
let sendVerificationEmail;
1711+
1712+
async function createTestUsers() {
1713+
const user = new Parse.User();
1714+
user.setUsername('testuser');
1715+
user.setPassword('password123');
1716+
user.set('email', 'unverified@example.com');
1717+
await user.signUp();
1718+
1719+
const user2 = new Parse.User();
1720+
user2.setUsername('verifieduser');
1721+
user2.setPassword('password123');
1722+
user2.set('email', 'verified@example.com');
1723+
await user2.signUp();
1724+
const config = Config.get(Parse.applicationId);
1725+
await config.database.update(
1726+
'_User',
1727+
{ username: 'verifieduser' },
1728+
{ emailVerified: true }
1729+
);
1730+
}
1731+
1732+
describe('default (emailVerifySuccessOnInvalidEmail: true)', () => {
1733+
beforeEach(async () => {
1734+
sendVerificationEmail = jasmine.createSpy('sendVerificationEmail');
1735+
await reconfigureServer({
1736+
appName: 'test',
1737+
publicServerURL: 'http://localhost:8378/1',
1738+
verifyUserEmails: true,
1739+
emailAdapter: {
1740+
sendVerificationEmail,
1741+
sendPasswordResetEmail: () => Promise.resolve(),
1742+
sendMail: () => {},
1743+
},
1744+
});
1745+
await createTestUsers();
1746+
});
1747+
it('returns success for non-existent email', async () => {
1748+
const response = await request({
1749+
url: 'http://localhost:8378/1/verificationEmailRequest',
1750+
method: 'POST',
1751+
body: { email: 'nonexistent@example.com' },
1752+
headers: {
1753+
'X-Parse-Application-Id': Parse.applicationId,
1754+
'X-Parse-REST-API-Key': 'rest',
1755+
'Content-Type': 'application/json',
1756+
},
1757+
});
1758+
expect(response.status).toBe(200);
1759+
expect(response.data).toEqual({});
1760+
});
1761+
1762+
it('returns success for already verified email', async () => {
1763+
const response = await request({
1764+
url: 'http://localhost:8378/1/verificationEmailRequest',
1765+
method: 'POST',
1766+
body: { email: 'verified@example.com' },
1767+
headers: {
1768+
'X-Parse-Application-Id': Parse.applicationId,
1769+
'X-Parse-REST-API-Key': 'rest',
1770+
'Content-Type': 'application/json',
1771+
},
1772+
});
1773+
expect(response.status).toBe(200);
1774+
expect(response.data).toEqual({});
1775+
});
1776+
1777+
it('returns success for unverified email', async () => {
1778+
const response = await request({
1779+
url: 'http://localhost:8378/1/verificationEmailRequest',
1780+
method: 'POST',
1781+
body: { email: 'unverified@example.com' },
1782+
headers: {
1783+
'X-Parse-Application-Id': Parse.applicationId,
1784+
'X-Parse-REST-API-Key': 'rest',
1785+
'Content-Type': 'application/json',
1786+
},
1787+
});
1788+
expect(response.status).toBe(200);
1789+
expect(response.data).toEqual({});
1790+
});
1791+
1792+
it('does not send verification email for non-existent email', async () => {
1793+
sendVerificationEmail.calls.reset();
1794+
await request({
1795+
url: 'http://localhost:8378/1/verificationEmailRequest',
1796+
method: 'POST',
1797+
body: { email: 'nonexistent@example.com' },
1798+
headers: {
1799+
'X-Parse-Application-Id': Parse.applicationId,
1800+
'X-Parse-REST-API-Key': 'rest',
1801+
'Content-Type': 'application/json',
1802+
},
1803+
});
1804+
expect(sendVerificationEmail).not.toHaveBeenCalled();
1805+
});
1806+
1807+
it('does not send verification email for already verified email', async () => {
1808+
sendVerificationEmail.calls.reset();
1809+
await request({
1810+
url: 'http://localhost:8378/1/verificationEmailRequest',
1811+
method: 'POST',
1812+
body: { email: 'verified@example.com' },
1813+
headers: {
1814+
'X-Parse-Application-Id': Parse.applicationId,
1815+
'X-Parse-REST-API-Key': 'rest',
1816+
'Content-Type': 'application/json',
1817+
},
1818+
});
1819+
expect(sendVerificationEmail).not.toHaveBeenCalled();
1820+
});
1821+
});
1822+
1823+
describe('opt-out (emailVerifySuccessOnInvalidEmail: false)', () => {
1824+
beforeEach(async () => {
1825+
sendVerificationEmail = jasmine.createSpy('sendVerificationEmail');
1826+
await reconfigureServer({
1827+
appName: 'test',
1828+
publicServerURL: 'http://localhost:8378/1',
1829+
verifyUserEmails: true,
1830+
emailVerifySuccessOnInvalidEmail: false,
1831+
emailAdapter: {
1832+
sendVerificationEmail,
1833+
sendPasswordResetEmail: () => Promise.resolve(),
1834+
sendMail: () => {},
1835+
},
1836+
});
1837+
await createTestUsers();
1838+
});
1839+
1840+
it('returns error for non-existent email', async () => {
1841+
const response = await request({
1842+
url: 'http://localhost:8378/1/verificationEmailRequest',
1843+
method: 'POST',
1844+
body: { email: 'nonexistent@example.com' },
1845+
headers: {
1846+
'X-Parse-Application-Id': Parse.applicationId,
1847+
'X-Parse-REST-API-Key': 'rest',
1848+
'Content-Type': 'application/json',
1849+
},
1850+
}).catch(e => e);
1851+
expect(response.data.code).toBe(Parse.Error.EMAIL_NOT_FOUND);
1852+
});
1853+
1854+
it('returns error for already verified email', async () => {
1855+
const response = await request({
1856+
url: 'http://localhost:8378/1/verificationEmailRequest',
1857+
method: 'POST',
1858+
body: { email: 'verified@example.com' },
1859+
headers: {
1860+
'X-Parse-Application-Id': Parse.applicationId,
1861+
'X-Parse-REST-API-Key': 'rest',
1862+
'Content-Type': 'application/json',
1863+
},
1864+
}).catch(e => e);
1865+
expect(response.status).not.toBe(200);
1866+
});
1867+
});
1868+
1869+
it('rejects invalid emailVerifySuccessOnInvalidEmail values', async () => {
1870+
const invalidValues = [[], {}, 1, 'string'];
1871+
for (const value of invalidValues) {
1872+
await expectAsync(
1873+
reconfigureServer({
1874+
appName: 'test',
1875+
publicServerURL: 'http://localhost:8378/1',
1876+
verifyUserEmails: true,
1877+
emailVerifySuccessOnInvalidEmail: value,
1878+
emailAdapter: {
1879+
sendVerificationEmail: () => {},
1880+
sendPasswordResetEmail: () => Promise.resolve(),
1881+
sendMail: () => {},
1882+
},
1883+
})
1884+
).toBeRejectedWith('emailVerifySuccessOnInvalidEmail must be a boolean value');
1885+
}
1886+
});
1887+
});

src/Config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export class Config {
200200
_publicServerURL,
201201
emailVerifyTokenValidityDuration,
202202
emailVerifyTokenReuseIfValid,
203+
emailVerifySuccessOnInvalidEmail,
203204
}) {
204205
const emailAdapter = userController.adapter;
205206
if (verifyUserEmails) {
@@ -209,6 +210,7 @@ export class Config {
209210
publicServerURL: publicServerURL || _publicServerURL,
210211
emailVerifyTokenValidityDuration,
211212
emailVerifyTokenReuseIfValid,
213+
emailVerifySuccessOnInvalidEmail,
212214
});
213215
}
214216
}
@@ -462,6 +464,7 @@ export class Config {
462464
) {
463465
throw 'resetPasswordSuccessOnInvalidEmail must be a boolean value';
464466
}
467+
465468
}
466469
}
467470

@@ -504,6 +507,7 @@ export class Config {
504507
publicServerURL,
505508
emailVerifyTokenValidityDuration,
506509
emailVerifyTokenReuseIfValid,
510+
emailVerifySuccessOnInvalidEmail,
507511
}) {
508512
if (!emailAdapter) {
509513
throw 'An emailAdapter is required for e-mail verification and password resets.';
@@ -525,6 +529,9 @@ export class Config {
525529
if (emailVerifyTokenReuseIfValid && !emailVerifyTokenValidityDuration) {
526530
throw 'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration';
527531
}
532+
if (emailVerifySuccessOnInvalidEmail && typeof emailVerifySuccessOnInvalidEmail !== 'boolean') {
533+
throw 'emailVerifySuccessOnInvalidEmail must be a boolean value';
534+
}
528535
}
529536

530537
static validateFileUploadOptions(fileUpload) {

src/Options/Definitions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ module.exports.ParseServerOptions = {
197197
help: 'Adapter module for email sending',
198198
action: parsers.moduleOrObjectParser,
199199
},
200+
emailVerifySuccessOnInvalidEmail: {
201+
env: 'PARSE_SERVER_EMAIL_VERIFY_SUCCESS_ON_INVALID_EMAIL',
202+
help: 'Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.<br><br>Default is `true`.<br>Requires option `verifyUserEmails: true`.',
203+
action: parsers.booleanParser,
204+
default: true,
205+
},
200206
emailVerifyTokenReuseIfValid: {
201207
env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID',
202208
help: 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.<br><br>Default is `false`.<br>Requires option `verifyUserEmails: true`.',

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@ export interface ParseServerOptions {
235235
Requires option `verifyUserEmails: true`.
236236
:DEFAULT: false */
237237
emailVerifyTokenReuseIfValid: ?boolean;
238+
/* Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.
239+
<br><br>
240+
Default is `true`.
241+
<br>
242+
Requires option `verifyUserEmails: true`.
243+
:DEFAULT: true */
244+
emailVerifySuccessOnInvalidEmail: ?boolean;
238245
/* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.
239246
<br><br>
240247
Default is `true`.

src/Routers/UsersRouter.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,13 @@ export class UsersRouter extends ClassesRouter {
547547
);
548548
}
549549

550+
const verifyEmailSuccessOnInvalidEmail = req.config.emailVerifySuccessOnInvalidEmail ?? true;
551+
550552
const results = await req.config.database.find('_User', { email: email }, {}, Auth.maintenance(req.config));
551553
if (!results.length || results.length < 1) {
554+
if (verifyEmailSuccessOnInvalidEmail) {
555+
return { response: {} };
556+
}
552557
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
553558
}
554559
const user = results[0];
@@ -557,6 +562,9 @@ export class UsersRouter extends ClassesRouter {
557562
delete user.password;
558563

559564
if (user.emailVerified) {
565+
if (verifyEmailSuccessOnInvalidEmail) {
566+
return { response: {} };
567+
}
560568
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
561569
}
562570

types/Options/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface ParseServerOptions {
9595
preventSignupWithUnverifiedEmail?: boolean;
9696
emailVerifyTokenValidityDuration?: number;
9797
emailVerifyTokenReuseIfValid?: boolean;
98+
emailVerifySuccessOnInvalidEmail?: boolean;
9899
sendUserEmailVerification?: boolean | ((params: SendEmailVerificationRequest) => boolean | Promise<boolean>);
99100
accountLockout?: AccountLockoutOptions;
100101
passwordPolicy?: PasswordPolicyOptions;

0 commit comments

Comments
 (0)