Skip to content

Commit b65e383

Browse files
authored
Merge branch 'alpha' into fix/directaccess-undefined-to-null
2 parents afaa510 + 383d3e5 commit b65e383

10 files changed

Lines changed: 504 additions & 7 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,9 @@ The following table lists all route groups covered by `routeAllowList` with exam
403403
> [!NOTE]
404404
> The GraphQL API is not covered by `routeAllowList`. `routeAllowList` gates the REST API per route, while every GraphQL operation is transported over a single endpoint with the operation, target class, and field set encoded in the request body — so per-route allow-list semantics do not compose with it.
405405
406+
> [!NOTE]
407+
> The Pages API is not covered by `routeAllowList`. Its routes (default endpoint `apps`, configurable via `pages.pagesEndpoint`) serve the browser pages for email verification and password reset that Parse Server links to in the emails it sends to end users, so they must remain reachable without any Parse credentials and are not part of the client REST API. Their behavior is controlled by the email verification and password reset options (`verifyUserEmails`, `emailAdapter`) and the `pages` option.
408+
406409
## Email Verification and Password Reset
407410

408411
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.

changelogs/CHANGELOG_alpha.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [9.10.1-alpha.9](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.8...9.10.1-alpha.9) (2026-09-09)
2+
3+
4+
### Bug Fixes
5+
6+
* Unauthenticated deletion of installation records via operator injection in device token deduplication ([GHSA-cc6h-c8m4-hgrx](https://github.com/parse-community/parse-server/security/advisories/GHSA-cc6h-c8m4-hgrx)) ([#10657](https://github.com/parse-community/parse-server/issues/10657)) ([ad00f82](https://github.com/parse-community/parse-server/commit/ad00f82d4545d550969baa560c34144831c0dd88))
7+
18
## [9.10.1-alpha.8](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.7...9.10.1-alpha.8) (2026-09-08)
29

310

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "9.10.1-alpha.8",
3+
"version": "9.10.1-alpha.9",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

spec/RouteAllowList.spec.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,122 @@ describe('routeAllowList', () => {
375375
});
376376
});
377377

378+
describe('Pages exemption', () => {
379+
// routeAllowList gates the client-facing REST API. The Pages API serves
380+
// the browser pages for email verification and password reset that Parse
381+
// Server links to in the emails it sends to end users, so those routes
382+
// must remain reachable without Parse credentials. The Pages router is
383+
// mounted ahead of the Parse request middleware chain and is not covered
384+
// by the allow list; its behavior is governed by the email verification
385+
// and password reset options instead.
386+
const request = require('../lib/request');
387+
const pagesConfig = () => ({
388+
appName: 'exampleAppName',
389+
publicServerURL: 'http://localhost:8378/1',
390+
verifyUserEmails: true,
391+
emailAdapter: {
392+
sendVerificationEmail: () => Promise.resolve(),
393+
sendPasswordResetEmail: () => Promise.resolve(),
394+
sendMail: () => {},
395+
},
396+
});
397+
const expectForbidden = promise =>
398+
expectAsync(promise).toBeRejectedWith(
399+
jasmine.objectContaining({
400+
data: jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN }),
401+
})
402+
);
403+
404+
it('reaches the email verification page when routeAllowList is empty array', async () => {
405+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
406+
await expectForbidden(request({ method: 'GET', url: 'http://localhost:8378/1/health' }));
407+
const response = await request({
408+
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken',
409+
followRedirects: false,
410+
});
411+
expect(response.status).toBe(200);
412+
expect(response.text).toContain('Invalid verification link!');
413+
});
414+
415+
it('reaches the password reset page when routeAllowList is empty array', async () => {
416+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
417+
await expectForbidden(
418+
request({
419+
method: 'POST',
420+
url: 'http://localhost:8378/1/requestPasswordReset',
421+
headers: {
422+
'X-Parse-Application-Id': 'test',
423+
'X-Parse-REST-API-Key': 'rest',
424+
'Content-Type': 'application/json',
425+
},
426+
body: JSON.stringify({ email: 'user@example.com' }),
427+
})
428+
);
429+
const response = await request({
430+
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=invalidToken',
431+
followRedirects: false,
432+
});
433+
expect(response.status).toBe(200);
434+
expect(response.text).toContain('Invalid password reset link!');
435+
});
436+
437+
it('reaches the resend verification email route when routeAllowList is empty array', async () => {
438+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
439+
const response = await request({
440+
method: 'POST',
441+
url: 'http://localhost:8378/1/apps/test/resend_verification_email',
442+
body: 'username=unknownUser',
443+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
444+
followRedirects: false,
445+
}).catch(e => e);
446+
expect(response.status).toBe(303);
447+
expect(response.headers.location).toContain('email_verification_send_success');
448+
});
449+
450+
it('reaches static pages when routeAllowList is empty array', async () => {
451+
await reconfigureServer({ ...pagesConfig(), routeAllowList: [] });
452+
const response = await request({
453+
url: 'http://localhost:8378/1/apps/password_reset.html',
454+
followRedirects: false,
455+
});
456+
expect(response.status).toBe(200);
457+
expect(response.text).toContain('Reset Your Password');
458+
});
459+
460+
it('reaches Pages routes when routeAllowList contains only REST routes', async () => {
461+
await reconfigureServer({ ...pagesConfig(), routeAllowList: ['classes/AllowedClass'] });
462+
const response = await request({
463+
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken',
464+
followRedirects: false,
465+
});
466+
expect(response.status).toBe(200);
467+
expect(response.text).toContain('Invalid verification link!');
468+
});
469+
470+
it('completes email verification from the emailed link when routeAllowList is empty array', async () => {
471+
const config = { ...pagesConfig(), routeAllowList: [] };
472+
await reconfigureServer(config);
473+
const sendVerificationEmail = spyOn(
474+
config.emailAdapter,
475+
'sendVerificationEmail'
476+
).and.callThrough();
477+
const user = new Parse.User();
478+
user.setUsername('exampleUsername');
479+
user.setPassword('examplePassword');
480+
user.set('email', 'user@example.com');
481+
await user.signUp(null, { useMasterKey: true });
482+
await jasmine.timeout();
483+
const link = sendVerificationEmail.calls.all()[0].args[0].link;
484+
const response = await request({ url: link, followRedirects: false });
485+
expect(response.status).toBe(200);
486+
expect(response.text).toContain('Email verified!');
487+
const verifiedUser = await new Parse.Query(Parse.User)
488+
.equalTo('username', 'exampleUsername')
489+
.first({ useMasterKey: true });
490+
expect(verifiedUser.get('emailVerified')).toBe(true);
491+
});
492+
});
493+
378494
describe('batch sub-requests', () => {
379495
// routeAllowList must be enforced per batch sub-request. The outer
380496
// enforceRouteAllowList middleware runs only on the outer /batch URL,

0 commit comments

Comments
 (0)