VYPR
Medium severityNVD Advisory· Published Jun 12, 2026

CVE-2026-50008

CVE-2026-50008

Description

Parse Server's routeAllowList can be bypassed via batch sub-requests, allowing access to restricted routes before version 9.9.1-alpha.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Parse Server's routeAllowList can be bypassed via batch sub-requests, allowing access to restricted routes before version 9.9.1-alpha.3.

Vulnerability

Affected versions: Parse Server from 9.8.0 to before 9.9.1-alpha.3. The routeAllowList server option is meant to restrict external client access to a configured list of REST API routes. However, the check is only enforced as Express middleware against the outer HTTP request URL. When a request hits the /batch handler, each sub-request is dispatched to the internal router without re-running the allow-list check [1][2]. This allows bypass of the configured route firewall.

Exploitation

An external attacker whose outer HTTP request matches the batch route can craft batch sub-requests to any REST API route that the operator omitted from the allow-list. No authentication bypass is achieved—authentication, ACL, CLP, and other authorization controls still apply—but the route-level firewall is circumvented. The attacker must have network access to the Parse Server and the ability to send batch requests (which is typically allowed if batch is in the allow-list) [2].

Impact

The impact is a bypass of the operator-configured route allow-list. Sensitive routes not meant to be exposed externally may become accessible via batch sub-requests. However, other authorization layers (authentication, ACLs, CLPs) still protect the data, so the attacker cannot bypass those unless already authenticated with sufficient permissions. The severity is medium.

Mitigation

The fix is implemented in version 9.9.1-alpha.3 [1]. Operators should upgrade to that version or later. For those who cannot upgrade immediately, a workaround is to explicitly include all inner routes that should be accessible via batch in the routeAllowList (e.g., routeAllowList: ['batch', 'classes/Public.*', 'functions/allowedFunction']). This eliminates the bypass but makes those routes reachable as direct REST requests as well [2]. Parse Server v8 LTS is not affected because routeAllowList was introduced in v9.8.0 [2].

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
552c6dd75463

fix: Server option routeAllowList is bypassable through batch sub-requests ([GHSA-p84r-h6rx-f2xr](https://github.com/parse-community/parse-server/security/advisories/GHSA-p84r-h6rx-f2xr)) (#10482)

https://github.com/parse-community/parse-serverManuelMay 27, 2026via body-scan-shorthand
3 files changed · +154 22
  • spec/RouteAllowList.spec.js+107 0 modified
    @@ -375,6 +375,113 @@ describe('routeAllowList', () => {
           });
         });
     
    +    describe('batch sub-requests', () => {
    +      // routeAllowList must be enforced per batch sub-request. The outer
    +      // enforceRouteAllowList middleware runs only on the outer /batch URL,
    +      // so without per-sub-request enforcement an operator who allowlists
    +      // `batch` would accidentally expose every REST route reachable through
    +      // batch sub-request dispatch.
    +      const restRequest = require('../lib/request');
    +      const headers = {
    +        'Content-Type': 'application/json',
    +        'X-Parse-Application-Id': 'test',
    +        'X-Parse-REST-API-Key': 'rest',
    +      };
    +
    +      it('blocks a batch GET sub-request whose path is not allowlisted', async () => {
    +        await reconfigureServer({ routeAllowList: ['batch'] });
    +        await new Parse.Object('Blocked').save({ secret: 'x' }, { useMasterKey: true });
    +        try {
    +          await restRequest({
    +            method: 'POST',
    +            headers,
    +            url: 'http://localhost:8378/1/batch',
    +            body: JSON.stringify({
    +              requests: [{ method: 'GET', path: '/1/classes/Blocked' }],
    +            }),
    +          });
    +          fail('batch sub-request to a blocked route should have been rejected');
    +        } catch (e) {
    +          expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
    +        }
    +      });
    +
    +      it('blocks a batch POST sub-request whose path is not allowlisted', async () => {
    +        await reconfigureServer({ routeAllowList: ['batch'] });
    +        try {
    +          await restRequest({
    +            method: 'POST',
    +            headers,
    +            url: 'http://localhost:8378/1/batch',
    +            body: JSON.stringify({
    +              requests: [{ method: 'POST', path: '/1/classes/Blocked', body: { x: 1 } }],
    +            }),
    +          });
    +          fail('batch sub-request POST to a blocked route should have been rejected');
    +        } catch (e) {
    +          expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
    +        }
    +        const query = new Parse.Query('Blocked');
    +        const results = await query.find({ useMasterKey: true });
    +        expect(results.length).toBe(0);
    +      });
    +
    +      it('allows a batch sub-request whose path matches the allow list', async () => {
    +        await reconfigureServer({ routeAllowList: ['batch', 'classes/Allowed'] });
    +        const response = await restRequest({
    +          method: 'POST',
    +          headers,
    +          url: 'http://localhost:8378/1/batch',
    +          body: JSON.stringify({
    +            requests: [{ method: 'POST', path: '/1/classes/Allowed', body: { x: 1 } }],
    +          }),
    +        });
    +        expect(response.data.length).toBe(1);
    +        expect(response.data[0].success.objectId).toBeDefined();
    +      });
    +
    +      it('rejects the entire batch if any sub-request is not allowlisted', async () => {
    +        await reconfigureServer({ routeAllowList: ['batch', 'classes/Allowed'] });
    +        try {
    +          await restRequest({
    +            method: 'POST',
    +            headers,
    +            url: 'http://localhost:8378/1/batch',
    +            body: JSON.stringify({
    +              requests: [
    +                { method: 'POST', path: '/1/classes/Allowed', body: { x: 1 } },
    +                { method: 'POST', path: '/1/classes/Blocked', body: { y: 2 } },
    +              ],
    +            }),
    +          });
    +          fail('batch with any disallowed sub-request should have been rejected');
    +        } catch (e) {
    +          expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
    +        }
    +        const allowedQuery = new Parse.Query('Allowed');
    +        const allowedResults = await allowedQuery.find({ useMasterKey: true });
    +        expect(allowedResults.length).toBe(0);
    +      });
    +
    +      it('allows master key to bypass sub-request allow-list check', async () => {
    +        await reconfigureServer({ routeAllowList: ['batch'] });
    +        const response = await restRequest({
    +          method: 'POST',
    +          headers: {
    +            'Content-Type': 'application/json',
    +            'X-Parse-Application-Id': 'test',
    +            'X-Parse-Master-Key': 'test',
    +          },
    +          url: 'http://localhost:8378/1/batch',
    +          body: JSON.stringify({
    +            requests: [{ method: 'POST', path: '/1/classes/Blocked', body: { x: 1 } }],
    +          }),
    +        });
    +        expect(response.data.length).toBe(1);
    +        expect(response.data[0].success.objectId).toBeDefined();
    +      });
    +    });
    +
         it_id('229cab22-dad3-4d08-8de5-64d813658596')(it)('should block all route groups when not in allow list', async () => {
           await reconfigureServer({
             routeAllowList: ['classes/GameScore'],
    
  • src/batch.js+13 0 modified
    @@ -1,5 +1,7 @@
     const Parse = require('parse/node').Parse;
     const path = require('path');
    +const { isRouteAllowed } = require('./middlewares');
    +const { createSanitizedError } = require('./Error');
     // These methods handle batch requests.
     const batchPath = '/batch';
     
    @@ -104,6 +106,17 @@ async function handleBatch(router, req) {
         if ((restRequest.method || 'GET').toUpperCase() === 'POST' && routablePath === batchPath) {
           throw new Parse.Error(Parse.Error.INVALID_JSON, 'nested batch requests are not allowed');
         }
    +    // Re-enforce routeAllowList on each sub-request. The enforceRouteAllowList
    +    // middleware runs once on the outer /batch URL, so without this check an
    +    // operator who allowlists `batch` would expose every route reachable via
    +    // sub-request dispatch.
    +    if (!isRouteAllowed(routablePath, req.config, req.auth)) {
    +      throw createSanitizedError(
    +        Parse.Error.OPERATION_FORBIDDEN,
    +        `Route not allowed by routeAllowList: ${(restRequest.method || 'GET').toUpperCase()} ${routablePath}`,
    +        req.config
    +      );
    +    }
         for (const limit of rateLimits) {
           const pathExp = limit.path.regexp || limit.path;
           if (!pathExp.test(routablePath)) {
    
  • src/middlewares.js+34 22 modified
    @@ -519,41 +519,53 @@ export function handleParseHealth(options) {
       };
     }
     
    -export function enforceRouteAllowList(req, res, next) {
    -  const config = req.config;
    -  if (!config || config.routeAllowList === undefined || config.routeAllowList === null) {
    -    return next();
    -  }
    -  if (req.auth && (req.auth.isMaster || req.auth.isMaintenance)) {
    -    return next();
    -  }
    -  let path = req.originalUrl;
    -  if (config.mount) {
    -    const mountPath = new URL(config.mount).pathname;
    -    if (path.startsWith(mountPath)) {
    -      path = path.substring(mountPath.length);
    +function normalizeRouteAllowListPath(path, mount) {
    +  let normalized = path;
    +  if (mount) {
    +    const mountPath = new URL(mount).pathname;
    +    if (normalized.startsWith(mountPath)) {
    +      normalized = normalized.substring(mountPath.length);
         }
       }
    -  if (path.startsWith('/')) {
    -    path = path.substring(1);
    +  if (normalized.startsWith('/')) {
    +    normalized = normalized.substring(1);
       }
    -  if (path.endsWith('/')) {
    -    path = path.substring(0, path.length - 1);
    +  if (normalized.endsWith('/')) {
    +    normalized = normalized.substring(0, normalized.length - 1);
       }
    -  const queryIndex = path.indexOf('?');
    +  const queryIndex = normalized.indexOf('?');
       if (queryIndex !== -1) {
    -    path = path.substring(0, queryIndex);
    +    normalized = normalized.substring(0, queryIndex);
       }
    +  return normalized;
    +}
    +
    +export function isRouteAllowed(path, config, auth) {
    +  if (!config || config.routeAllowList === undefined || config.routeAllowList === null) {
    +    return true;
    +  }
    +  if (auth && (auth.isMaster || auth.isMaintenance)) {
    +    return true;
    +  }
    +  const normalized = normalizeRouteAllowListPath(path, config.mount);
       const regexes = config._routeAllowListRegex || [];
       for (const regex of regexes) {
    -    if (regex.test(path)) {
    -      return next();
    +    if (regex.test(normalized)) {
    +      return true;
         }
       }
    +  return false;
    +}
    +
    +export function enforceRouteAllowList(req, res, next) {
    +  if (isRouteAllowed(req.originalUrl, req.config, req.auth)) {
    +    return next();
    +  }
    +  const path = normalizeRouteAllowListPath(req.originalUrl, req.config?.mount);
       throw createSanitizedError(
         Parse.Error.OPERATION_FORBIDDEN,
         `Route not allowed by routeAllowList: ${req.method} ${path}`,
    -    config
    +    req.config
       );
     }
     
    

Vulnerability mechanics

Root cause

"The routeAllowList check is only enforced as Express middleware against the outer HTTP request URL, so the /batch handler dispatches each sub-request to the internal router without re-running the allow-list check."

Attack vector

An external attacker sends a POST request to the `/batch` endpoint with a JSON body containing sub-requests that target REST API routes omitted from the operator-configured `routeAllowList`. Because the allow-list middleware only inspects the outer request URL (which matches `batch`), the batch handler dispatches each sub-request without re-checking the allow-list. This bypasses the operator's route firewall, though authentication, ACLs, CLPs, and other inner-route authorization controls still apply.

Affected code

The vulnerability resides in `src/batch.js` and `src/middlewares.js`. The `enforceRouteAllowList` middleware only runs against the outer HTTP request URL, so a `/batch` request passes the allow-list check without the sub-requests being re-evaluated. The patch adds a call to `isRouteAllowed` inside `handleBatch` in `src/batch.js` to enforce the allow-list per sub-request.

What the fix does

The patch refactors the allow-list logic in `src/middlewares.js` by extracting a reusable `isRouteAllowed` function and a `normalizeRouteAllowListPath` helper. In `src/batch.js`, the `handleBatch` function now calls `isRouteAllowed` for each sub-request's routable path before dispatching it; if the path is not allowlisted, an `OPERATION_FORBIDDEN` error is thrown. This ensures that the operator-configured route firewall is enforced even when requests arrive through the batch endpoint.

Preconditions

  • configThe server must have `routeAllowList` configured and include `batch` in the allowed list.
  • networkThe attacker must be able to send HTTP requests to the Parse Server's `/batch` endpoint.
  • authThe attacker does not need authentication to trigger the bypass, but inner-route authorization (ACLs, CLPs) still applies to the sub-requests.

Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.