VYPR
High severityNVD Advisory· Published Mar 24, 2026· Updated Mar 26, 2026

Parse Server: SQL injection via aggregate and distinct field names in PostgreSQL adapter

CVE-2026-33539

Description

Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.59 and 9.6.0-alpha.53, an attacker with master key access can execute arbitrary SQL statements on the PostgreSQL database by injecting SQL metacharacters into field name parameters of the aggregate $group pipeline stage or the distinct operation. This allows privilege escalation from Parse Server application-level administrator to PostgreSQL database-level access. Only Parse Server deployments using PostgreSQL are affected. MongoDB deployments are not affected. This issue has been patched in versions 8.6.59 and 9.6.0-alpha.53.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
>= 9.0.0, < 9.6.0-alpha.539.6.0-alpha.53
parse-servernpm
< 8.6.598.6.59

Affected products

1

Patches

2
bdddab5f8b61

fix: SQL injection via aggregate and distinct field names in PostgreSQL adapter ([GHSA-p2w6-rmh7-w8q3](https://github.com/parse-community/parse-server/security/advisories/GHSA-p2w6-rmh7-w8q3)) (#10272)

3 files changed · +206 2
  • spec/vulnerabilities.spec.js+184 0 modified
    @@ -4206,3 +4206,187 @@ describe('(GHSA-g4cf-xj29-wqqr) DoS via unindexed database query for unconfigure
         expect(authDataQueries.length).toBeGreaterThan(0);
       });
     });
    +
    +describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field names in PostgreSQL adapter', () => {
    +  const headers = {
    +    'Content-Type': 'application/json',
    +    'X-Parse-Application-Id': 'test',
    +    'X-Parse-REST-API-Key': 'rest',
    +    'X-Parse-Master-Key': 'test',
    +  };
    +  const serverURL = 'http://localhost:8378/1';
    +
    +  beforeEach(async () => {
    +    const obj = new Parse.Object('TestClass');
    +    obj.set('playerName', 'Alice');
    +    obj.set('score', 100);
    +    obj.set('metadata', { tag: 'hello' });
    +    await obj.save(null, { useMasterKey: true });
    +  });
    +
    +  describe('aggregate $group._id SQL injection', () => {
    +    it_only_db('postgres')('rejects $group._id field value containing double quotes', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  alias: '$playerName" OR 1=1 --',
    +                },
    +              },
    +            },
    +          ]),
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects $group._id field value containing semicolons', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  alias: '$playerName"; DROP TABLE "TestClass" --',
    +                },
    +              },
    +            },
    +          ]),
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects $group._id date operation field value containing double quotes', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  day: { $dayOfMonth: '$createdAt" OR 1=1 --' },
    +                },
    +              },
    +            },
    +          ]),
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate $group._id with field reference', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  name: '$playerName',
    +                },
    +                count: { $sum: 1 },
    +              },
    +            },
    +          ]),
    +        },
    +      });
    +      expect(response.data?.results?.length).toBeGreaterThan(0);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate $group._id with date extraction', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  day: { $dayOfMonth: '$_created_at' },
    +                },
    +                count: { $sum: 1 },
    +              },
    +            },
    +          ]),
    +        },
    +      });
    +      expect(response.data?.results?.length).toBeGreaterThan(0);
    +    });
    +  });
    +
    +  describe('distinct dot-notation SQL injection', () => {
    +    it_only_db('postgres')('rejects distinct field name containing double quotes in dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'metadata" FROM pg_tables; --.tag',
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects distinct field name containing semicolons in dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'metadata; DROP TABLE "TestClass" --.tag',
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects distinct field name containing single quotes in dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: "metadata' OR '1'='1.tag",
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate distinct with dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'metadata.tag',
    +        },
    +      });
    +      expect(response.data?.results).toEqual(['hello']);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate distinct without dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'playerName',
    +        },
    +      });
    +      expect(response.data?.results).toEqual(['Alice']);
    +    });
    +  });
    +});
    
  • src/Adapters/Storage/Postgres/PostgresStorageAdapter.js+19 2 modified
    @@ -234,6 +234,12 @@ const transformDotField = fieldName => {
       return name;
     };
     
    +const validateAggregateFieldName = name => {
    +  if (typeof name !== 'string' || !name.match(/^[a-zA-Z][a-zA-Z0-9_]*$/)) {
    +    throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${name}`);
    +  }
    +};
    +
     const transformAggregateField = fieldName => {
       if (typeof fieldName !== 'string') {
         return fieldName;
    @@ -244,7 +250,12 @@ const transformAggregateField = fieldName => {
       if (fieldName === '$_updated_at') {
         return 'updatedAt';
       }
    -  return fieldName.substring(1);
    +  if (!fieldName.startsWith('$')) {
    +    throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}`);
    +  }
    +  const name = fieldName.substring(1);
    +  validateAggregateFieldName(name);
    +  return name;
     };
     
     const validateKeys = object => {
    @@ -2179,12 +2190,18 @@ export class PostgresStorageAdapter implements StorageAdapter {
     
       async distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) {
         debug('distinct');
    +    const fieldSegments = fieldName.split('.');
    +    for (const segment of fieldSegments) {
    +      if (!segment.match(/^[a-zA-Z][a-zA-Z0-9_]*$/)) {
    +        throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}`);
    +      }
    +    }
         let field = fieldName;
         let column = fieldName;
         const isNested = fieldName.indexOf('.') >= 0;
         if (isNested) {
           field = transformDotFieldToComponents(fieldName).join('->');
    -      column = fieldName.split('.')[0];
    +      column = fieldSegments[0];
         }
         const isArrayField =
           schema.fields && schema.fields[fieldName] && schema.fields[fieldName].type === 'Array';
    
  • src/Routers/AggregateRouter.js+3 0 modified
    @@ -52,6 +52,9 @@ export class AggregateRouter extends ClassesRouter {
           }
           return { response };
         } catch (e) {
    +      if (e instanceof Parse.Error) {
    +        throw e;
    +      }
           throw new Parse.Error(Parse.Error.INVALID_QUERY, e.message);
         }
       }
    
03249f9bf5b8

fix: SQL injection via aggregate and distinct field names in PostgreSQL adapter ([GHSA-p2w6-rmh7-w8q3](https://github.com/parse-community/parse-server/security/advisories/GHSA-p2w6-rmh7-w8q3)) (#10273)

3 files changed · +206 2
  • spec/vulnerabilities.spec.js+184 0 modified
    @@ -3780,3 +3780,187 @@ describe('(GHSA-g4cf-xj29-wqqr) DoS via unindexed database query for unconfigure
         expect(authDataQueries.length).toBeGreaterThan(0);
       });
     });
    +
    +describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field names in PostgreSQL adapter', () => {
    +  const headers = {
    +    'Content-Type': 'application/json',
    +    'X-Parse-Application-Id': 'test',
    +    'X-Parse-REST-API-Key': 'rest',
    +    'X-Parse-Master-Key': 'test',
    +  };
    +  const serverURL = 'http://localhost:8378/1';
    +
    +  beforeEach(async () => {
    +    const obj = new Parse.Object('TestClass');
    +    obj.set('playerName', 'Alice');
    +    obj.set('score', 100);
    +    obj.set('metadata', { tag: 'hello' });
    +    await obj.save(null, { useMasterKey: true });
    +  });
    +
    +  describe('aggregate $group._id SQL injection', () => {
    +    it_only_db('postgres')('rejects $group._id field value containing double quotes', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  alias: '$playerName" OR 1=1 --',
    +                },
    +              },
    +            },
    +          ]),
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects $group._id field value containing semicolons', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  alias: '$playerName"; DROP TABLE "TestClass" --',
    +                },
    +              },
    +            },
    +          ]),
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects $group._id date operation field value containing double quotes', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  day: { $dayOfMonth: '$createdAt" OR 1=1 --' },
    +                },
    +              },
    +            },
    +          ]),
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate $group._id with field reference', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  name: '$playerName',
    +                },
    +                count: { $sum: 1 },
    +              },
    +            },
    +          ]),
    +        },
    +      });
    +      expect(response.data?.results?.length).toBeGreaterThan(0);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate $group._id with date extraction', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          pipeline: JSON.stringify([
    +            {
    +              $group: {
    +                _id: {
    +                  day: { $dayOfMonth: '$_created_at' },
    +                },
    +                count: { $sum: 1 },
    +              },
    +            },
    +          ]),
    +        },
    +      });
    +      expect(response.data?.results?.length).toBeGreaterThan(0);
    +    });
    +  });
    +
    +  describe('distinct dot-notation SQL injection', () => {
    +    it_only_db('postgres')('rejects distinct field name containing double quotes in dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'metadata" FROM pg_tables; --.tag',
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects distinct field name containing semicolons in dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'metadata; DROP TABLE "TestClass" --.tag',
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('rejects distinct field name containing single quotes in dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: "metadata' OR '1'='1.tag",
    +        },
    +      }).catch(e => e);
    +      expect(response.data?.code).toBe(Parse.Error.INVALID_KEY_NAME);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate distinct with dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'metadata.tag',
    +        },
    +      });
    +      expect(response.data?.results).toEqual(['hello']);
    +    });
    +
    +    it_only_db('postgres')('allows legitimate distinct without dot notation', async () => {
    +      const response = await request({
    +        method: 'GET',
    +        url: `${serverURL}/aggregate/TestClass`,
    +        headers,
    +        qs: {
    +          distinct: 'playerName',
    +        },
    +      });
    +      expect(response.data?.results).toEqual(['Alice']);
    +    });
    +  });
    +});
    
  • src/Adapters/Storage/Postgres/PostgresStorageAdapter.js+19 2 modified
    @@ -233,6 +233,12 @@ const transformDotField = fieldName => {
       return name;
     };
     
    +const validateAggregateFieldName = name => {
    +  if (typeof name !== 'string' || !name.match(/^[a-zA-Z][a-zA-Z0-9_]*$/)) {
    +    throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${name}`);
    +  }
    +};
    +
     const transformAggregateField = fieldName => {
       if (typeof fieldName !== 'string') {
         return fieldName;
    @@ -243,7 +249,12 @@ const transformAggregateField = fieldName => {
       if (fieldName === '$_updated_at') {
         return 'updatedAt';
       }
    -  return fieldName.substring(1);
    +  if (!fieldName.startsWith('$')) {
    +    throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}`);
    +  }
    +  const name = fieldName.substring(1);
    +  validateAggregateFieldName(name);
    +  return name;
     };
     
     const validateKeys = object => {
    @@ -2157,12 +2168,18 @@ export class PostgresStorageAdapter implements StorageAdapter {
     
       async distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) {
         debug('distinct');
    +    const fieldSegments = fieldName.split('.');
    +    for (const segment of fieldSegments) {
    +      if (!segment.match(/^[a-zA-Z][a-zA-Z0-9_]*$/)) {
    +        throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}`);
    +      }
    +    }
         let field = fieldName;
         let column = fieldName;
         const isNested = fieldName.indexOf('.') >= 0;
         if (isNested) {
           field = transformDotFieldToComponents(fieldName).join('->');
    -      column = fieldName.split('.')[0];
    +      column = fieldSegments[0];
         }
         const isArrayField =
           schema.fields && schema.fields[fieldName] && schema.fields[fieldName].type === 'Array';
    
  • src/Routers/AggregateRouter.js+3 0 modified
    @@ -48,6 +48,9 @@ export class AggregateRouter extends ClassesRouter {
           }
           return { response };
         } catch (e) {
    +      if (e instanceof Parse.Error) {
    +        throw e;
    +      }
           throw new Parse.Error(Parse.Error.INVALID_QUERY, e.message);
         }
       }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.