-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
fix: NoSQL injection via token type in password reset and email verification endpoints (GHSA-vgjh-hmwf-c588) #10128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mtrezza
merged 50 commits into
parse-community:alpha
from
mtrezza:fix/GHSA-vgjh-hmwf-c588-v9
Mar 7, 2026
+150
−1
Merged
Changes from all commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
833cf02
Update pull_request_template.md
mtrezza 1aecff9
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza afabddd
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 299d834
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza d28f548
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 04b5eb4
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza edb54cb
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 680e0dd
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 63dc9ab
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 77c1f5f
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 484260f
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza f6d1541
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 55a273d
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 1e78abc
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 5efa515
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 7be0502
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 1ba83ba
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 049c717
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 2ade87b
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 9646308
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza c92859e
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 59c98f0
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza f0a0445
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza ec310c2
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 174f0a1
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza c0434cb
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 48ea04b
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza cde66d5
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 99c2065
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 85cb005
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 26b92fa
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 0806873
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 1f65cf4
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza b8a0a98
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 6b96efe
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 3f20e0c
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 94c77b6
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza df5ac7d
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 05ca77a
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza d70d3c7
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 1f9c44c
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza b98cce5
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 529912a
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 455f4ef
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 6e47cc1
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza fed09fe
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 815e1f0
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza 729362a
Merge remote-tracking branch 'upstream/alpha' into alpha
mtrezza fb69186
fix
mtrezza cf72215
fix
mtrezza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -692,4 +692,5 @@ describe('relativeTimeToDate', () => { | |
| }); | ||
| }); | ||
| }); | ||
|
|
||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,6 +125,149 @@ describe('Regex Vulnerabilities', () => { | |
| }); | ||
| }); | ||
|
|
||
| describe('on password reset request via token (handleResetRequest)', () => { | ||
| beforeEach(async () => { | ||
| user = await Parse.User.logIn('[email protected]', '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: '[email protected]', | ||
| }), | ||
| }); | ||
| // 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(); | ||
| const goToPage = 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(); | ||
| const goToPage = 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('[email protected]', 'somepassword'); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.