diff --git a/README.md b/README.md index 047642a64b..f8b397b910 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,9 @@ The following table lists all route groups covered by `routeAllowList` with exam > [!NOTE] > 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. +> [!NOTE] +> 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. + ## Email Verification and Password Reset 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. diff --git a/spec/RouteAllowList.spec.js b/spec/RouteAllowList.spec.js index 669973d371..3264a21ae8 100644 --- a/spec/RouteAllowList.spec.js +++ b/spec/RouteAllowList.spec.js @@ -375,6 +375,122 @@ describe('routeAllowList', () => { }); }); + describe('Pages exemption', () => { + // routeAllowList gates the client-facing REST API. The Pages API serves + // the browser pages for email verification and password reset that Parse + // Server links to in the emails it sends to end users, so those routes + // must remain reachable without Parse credentials. The Pages router is + // mounted ahead of the Parse request middleware chain and is not covered + // by the allow list; its behavior is governed by the email verification + // and password reset options instead. + const request = require('../lib/request'); + const pagesConfig = () => ({ + appName: 'exampleAppName', + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + }); + const expectForbidden = promise => + expectAsync(promise).toBeRejectedWith( + jasmine.objectContaining({ + data: jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN }), + }) + ); + + it('reaches the email verification page when routeAllowList is empty array', async () => { + await reconfigureServer({ ...pagesConfig(), routeAllowList: [] }); + await expectForbidden(request({ method: 'GET', url: 'http://localhost:8378/1/health' })); + const response = await request({ + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken', + followRedirects: false, + }); + expect(response.status).toBe(200); + expect(response.text).toContain('Invalid verification link!'); + }); + + it('reaches the password reset page when routeAllowList is empty array', async () => { + await reconfigureServer({ ...pagesConfig(), routeAllowList: [] }); + await expectForbidden( + request({ + method: 'POST', + url: 'http://localhost:8378/1/requestPasswordReset', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: 'user@example.com' }), + }) + ); + const response = await request({ + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=invalidToken', + followRedirects: false, + }); + expect(response.status).toBe(200); + expect(response.text).toContain('Invalid password reset link!'); + }); + + it('reaches the resend verification email route when routeAllowList is empty array', async () => { + await reconfigureServer({ ...pagesConfig(), routeAllowList: [] }); + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/resend_verification_email', + body: 'username=unknownUser', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(303); + expect(response.headers.location).toContain('email_verification_send_success'); + }); + + it('reaches static pages when routeAllowList is empty array', async () => { + await reconfigureServer({ ...pagesConfig(), routeAllowList: [] }); + const response = await request({ + url: 'http://localhost:8378/1/apps/password_reset.html', + followRedirects: false, + }); + expect(response.status).toBe(200); + expect(response.text).toContain('Reset Your Password'); + }); + + it('reaches Pages routes when routeAllowList contains only REST routes', async () => { + await reconfigureServer({ ...pagesConfig(), routeAllowList: ['classes/AllowedClass'] }); + const response = await request({ + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken', + followRedirects: false, + }); + expect(response.status).toBe(200); + expect(response.text).toContain('Invalid verification link!'); + }); + + it('completes email verification from the emailed link when routeAllowList is empty array', async () => { + const config = { ...pagesConfig(), routeAllowList: [] }; + await reconfigureServer(config); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'user@example.com'); + await user.signUp(null, { useMasterKey: true }); + await jasmine.timeout(); + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const response = await request({ url: link, followRedirects: false }); + expect(response.status).toBe(200); + expect(response.text).toContain('Email verified!'); + const verifiedUser = await new Parse.Query(Parse.User) + .equalTo('username', 'exampleUsername') + .first({ useMasterKey: true }); + expect(verifiedUser.get('emailVerified')).toBe(true); + }); + }); + describe('batch sub-requests', () => { // routeAllowList must be enforced per batch sub-request. The outer // enforceRouteAllowList middleware runs only on the outer /batch URL, diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 64c54561eb..1322ac3f2e 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -589,7 +589,7 @@ module.exports.ParseServerOptions = { }, routeAllowList: { env: 'PARSE_SERVER_ROUTE_ALLOW_LIST', - help: '(Optional) Restricts external client access to a list of allowed REST API routes.

When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.

Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.

Examples of normalized route identifiers:Example patterns:Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).

When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.

Defaults to `undefined` which means the feature is inactive and all routes are accessible.

Note: File routes and the GraphQL API are not covered by this option.', + help: '(Optional) Restricts external client access to a list of allowed REST API routes.

When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.

Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.

Examples of normalized route identifiers:Example patterns:Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).

When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.

Defaults to `undefined` which means the feature is inactive and all routes are accessible.

Note: File routes, the Pages API and the GraphQL API are not covered by this option.', action: parsers.arrayParser, }, scheduledPush: { diff --git a/src/Options/docs.js b/src/Options/docs.js index 564a9b1718..aad201e454 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -106,7 +106,7 @@ * @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns. * @property {String} restAPIKey Key for REST calls * @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. - * @property {String[]} routeAllowList (Optional) Restricts external client access to a list of allowed REST API routes.

When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.

Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.

Examples of normalized route identifiers:Example patterns:Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).

When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.

Defaults to `undefined` which means the feature is inactive and all routes are accessible.

Note: File routes and the GraphQL API are not covered by this option. + * @property {String[]} routeAllowList (Optional) Restricts external client access to a list of allowed REST API routes.

When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.

Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.

Examples of normalized route identifiers:Example patterns:Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).

When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.

Defaults to `undefined` which means the feature is inactive and all routes are accessible.

Note: File routes, the Pages API and the GraphQL API are not covered by this option. * @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false. * @property {SchemaOptions} schema Defined schema * @property {SecurityOptions} security The security options to identify and report weak security settings. diff --git a/src/Options/index.js b/src/Options/index.js index 42f0ffb3b8..4af4c792b0 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -79,7 +79,7 @@ export interface ParseServerOptions { /* (Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key. :DEFAULT: ["127.0.0.1","::1"] */ masterKeyIps: ?(string[]); - /* (Optional) Restricts external client access to a list of allowed REST API routes.

When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.

Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.

Examples of normalized route identifiers:Example patterns:Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).

When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.

Defaults to `undefined` which means the feature is inactive and all routes are accessible.

Note: File routes and the GraphQL API are not covered by this option.*/ + /* (Optional) Restricts external client access to a list of allowed REST API routes.

When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.

Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.

Examples of normalized route identifiers:Example patterns:Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).

When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.

Defaults to `undefined` which means the feature is inactive and all routes are accessible.

Note: File routes, the Pages API and the GraphQL API are not covered by this option.*/ routeAllowList: ?(string[]); /* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key. :DEFAULT: ["127.0.0.1","::1"] */