mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: SQL injection via dot-notation field name in PostgreSQL ([GHSA-qpr4-jrj4-6f27](https://github.com/parse-community/parse-server/security/advisories/GHSA-qpr4-jrj4-6f27)) (#10160)
This commit is contained in:
@@ -817,3 +817,164 @@ describe('Postgres regex sanitizater', () => {
|
|||||||
expect(response.data.results.length).toBe(0);
|
expect(response.data.results.length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('(GHSA-qpr4-jrj4-6f27) SQL Injection via sort dot-notation field name', () => {
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest',
|
||||||
|
};
|
||||||
|
|
||||||
|
it_only_db('postgres')('does not execute injected SQL via sort order dot-notation', async () => {
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
obj.set('name', 'original');
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
// This payload would execute a stacked query if single quotes are not escaped
|
||||||
|
await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = 'hacked' WHERE true--",
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// Verify the data was not modified by injected SQL
|
||||||
|
const verify = await new Parse.Query('InjectionTest').get(obj.id);
|
||||||
|
expect(verify.get('name')).toBe('original');
|
||||||
|
});
|
||||||
|
|
||||||
|
it_only_db('postgres')('does not execute injected SQL via sort order with pg_sleep', async () => {
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: "data.x' ASC; SELECT pg_sleep(3)--",
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
|
||||||
|
// If injection succeeded, query would take >= 3 seconds
|
||||||
|
expect(elapsed).toBeLessThan(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it_only_db('postgres')('does not execute injection via dollar-sign quoting bypass', async () => {
|
||||||
|
// PostgreSQL supports $$string$$ as alternative to 'string'
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
obj.set('name', 'original');
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = $$hacked$$ WHERE true--",
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
const verify = await new Parse.Query('InjectionTest').get(obj.id);
|
||||||
|
expect(verify.get('name')).toBe('original');
|
||||||
|
});
|
||||||
|
|
||||||
|
it_only_db('postgres')('does not execute injection via tagged dollar quoting bypass', async () => {
|
||||||
|
// PostgreSQL supports $tag$string$tag$ as alternative to 'string'
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
obj.set('name', 'original');
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = $t$hacked$t$ WHERE true--",
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
const verify = await new Parse.Query('InjectionTest').get(obj.id);
|
||||||
|
expect(verify.get('name')).toBe('original');
|
||||||
|
});
|
||||||
|
|
||||||
|
it_only_db('postgres')('does not execute injection via CHR() concatenation bypass', async () => {
|
||||||
|
// CHR(104)||CHR(97)||... builds 'hacked' without quotes
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
obj.set('name', 'original');
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = CHR(104)||CHR(97)||CHR(99)||CHR(107) WHERE true--",
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
const verify = await new Parse.Query('InjectionTest').get(obj.id);
|
||||||
|
expect(verify.get('name')).toBe('original');
|
||||||
|
});
|
||||||
|
|
||||||
|
it_only_db('postgres')('does not execute injection via backslash escape bypass', async () => {
|
||||||
|
// Backslash before quote could interact with '' escaping in some configurations
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
obj.set('name', 'original');
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: "data.x\\' ASC; UPDATE \"InjectionTest\" SET name = 'hacked' WHERE true--",
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
const verify = await new Parse.Query('InjectionTest').get(obj.id);
|
||||||
|
expect(verify.get('name')).toBe('original');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows valid dot-notation sort on object field', async () => {
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { key: 'value' });
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: 'data.key',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows valid dot-notation with special characters in sub-field', async () => {
|
||||||
|
const obj = new Parse.Object('InjectionTest');
|
||||||
|
obj.set('data', { 'my-field': 'value' });
|
||||||
|
await obj.save();
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://localhost:8378/1/classes/InjectionTest',
|
||||||
|
headers,
|
||||||
|
qs: {
|
||||||
|
order: 'data.my-field',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -211,12 +211,12 @@ const handleDotFields = object => {
|
|||||||
const transformDotFieldToComponents = fieldName => {
|
const transformDotFieldToComponents = fieldName => {
|
||||||
return fieldName.split('.').map((cmpt, index) => {
|
return fieldName.split('.').map((cmpt, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return `"${cmpt}"`;
|
return `"${cmpt.replace(/"/g, '""')}"`;
|
||||||
}
|
}
|
||||||
if (isArrayIndex(cmpt)) {
|
if (isArrayIndex(cmpt)) {
|
||||||
return Number(cmpt);
|
return Number(cmpt);
|
||||||
} else {
|
} else {
|
||||||
return `'${cmpt}'`;
|
return `'${cmpt.replace(/'/g, "''")}'`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user