mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
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)
This commit is contained in:
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user