Skip to content

Commit ea538a4

Browse files
authored
fix: SQL injection via dot-notation field name in PostgreSQL ([GHSA-qpr4-jrj4-6f27](GHSA-qpr4-jrj4-6f27)) (#10159)
1 parent df80c89 commit ea538a4

File tree

2 files changed

+163
-2
lines changed

2 files changed

+163
-2
lines changed

spec/vulnerabilities.spec.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,167 @@ describe('(GHSA-mf3j-86qx-cq5j) ReDoS via $regex in LiveQuery subscription', ()
860860
});
861861
});
862862

863+
describe('(GHSA-qpr4-jrj4-6f27) SQL Injection via sort dot-notation field name', () => {
864+
const headers = {
865+
'Content-Type': 'application/json',
866+
'X-Parse-Application-Id': 'test',
867+
'X-Parse-REST-API-Key': 'rest',
868+
};
869+
870+
it_only_db('postgres')('does not execute injected SQL via sort order dot-notation', async () => {
871+
const obj = new Parse.Object('InjectionTest');
872+
obj.set('data', { key: 'value' });
873+
obj.set('name', 'original');
874+
await obj.save();
875+
876+
// This payload would execute a stacked query if single quotes are not escaped
877+
await request({
878+
method: 'GET',
879+
url: 'http://localhost:8378/1/classes/InjectionTest',
880+
headers,
881+
qs: {
882+
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = 'hacked' WHERE true--",
883+
},
884+
}).catch(() => {});
885+
886+
// Verify the data was not modified by injected SQL
887+
const verify = await new Parse.Query('InjectionTest').get(obj.id);
888+
expect(verify.get('name')).toBe('original');
889+
});
890+
891+
it_only_db('postgres')('does not execute injected SQL via sort order with pg_sleep', async () => {
892+
const obj = new Parse.Object('InjectionTest');
893+
obj.set('data', { key: 'value' });
894+
await obj.save();
895+
896+
const start = Date.now();
897+
await request({
898+
method: 'GET',
899+
url: 'http://localhost:8378/1/classes/InjectionTest',
900+
headers,
901+
qs: {
902+
order: "data.x' ASC; SELECT pg_sleep(3)--",
903+
},
904+
}).catch(() => {});
905+
const elapsed = Date.now() - start;
906+
907+
// If injection succeeded, query would take >= 3 seconds
908+
expect(elapsed).toBeLessThan(3000);
909+
});
910+
911+
it_only_db('postgres')('does not execute injection via dollar-sign quoting bypass', async () => {
912+
// PostgreSQL supports $$string$$ as alternative to 'string'
913+
const obj = new Parse.Object('InjectionTest');
914+
obj.set('data', { key: 'value' });
915+
obj.set('name', 'original');
916+
await obj.save();
917+
918+
await request({
919+
method: 'GET',
920+
url: 'http://localhost:8378/1/classes/InjectionTest',
921+
headers,
922+
qs: {
923+
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = $$hacked$$ WHERE true--",
924+
},
925+
}).catch(() => {});
926+
927+
const verify = await new Parse.Query('InjectionTest').get(obj.id);
928+
expect(verify.get('name')).toBe('original');
929+
});
930+
931+
it_only_db('postgres')('does not execute injection via tagged dollar quoting bypass', async () => {
932+
// PostgreSQL supports $tag$string$tag$ as alternative to 'string'
933+
const obj = new Parse.Object('InjectionTest');
934+
obj.set('data', { key: 'value' });
935+
obj.set('name', 'original');
936+
await obj.save();
937+
938+
await request({
939+
method: 'GET',
940+
url: 'http://localhost:8378/1/classes/InjectionTest',
941+
headers,
942+
qs: {
943+
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = $t$hacked$t$ WHERE true--",
944+
},
945+
}).catch(() => {});
946+
947+
const verify = await new Parse.Query('InjectionTest').get(obj.id);
948+
expect(verify.get('name')).toBe('original');
949+
});
950+
951+
it_only_db('postgres')('does not execute injection via CHR() concatenation bypass', async () => {
952+
// CHR(104)||CHR(97)||... builds 'hacked' without quotes
953+
const obj = new Parse.Object('InjectionTest');
954+
obj.set('data', { key: 'value' });
955+
obj.set('name', 'original');
956+
await obj.save();
957+
958+
await request({
959+
method: 'GET',
960+
url: 'http://localhost:8378/1/classes/InjectionTest',
961+
headers,
962+
qs: {
963+
order: "data.x' ASC; UPDATE \"InjectionTest\" SET name = CHR(104)||CHR(97)||CHR(99)||CHR(107) WHERE true--",
964+
},
965+
}).catch(() => {});
966+
967+
const verify = await new Parse.Query('InjectionTest').get(obj.id);
968+
expect(verify.get('name')).toBe('original');
969+
});
970+
971+
it_only_db('postgres')('does not execute injection via backslash escape bypass', async () => {
972+
// Backslash before quote could interact with '' escaping in some configurations
973+
const obj = new Parse.Object('InjectionTest');
974+
obj.set('data', { key: 'value' });
975+
obj.set('name', 'original');
976+
await obj.save();
977+
978+
await request({
979+
method: 'GET',
980+
url: 'http://localhost:8378/1/classes/InjectionTest',
981+
headers,
982+
qs: {
983+
order: "data.x\\' ASC; UPDATE \"InjectionTest\" SET name = 'hacked' WHERE true--",
984+
},
985+
}).catch(() => {});
986+
987+
const verify = await new Parse.Query('InjectionTest').get(obj.id);
988+
expect(verify.get('name')).toBe('original');
989+
});
990+
991+
it('allows valid dot-notation sort on object field', async () => {
992+
const obj = new Parse.Object('InjectionTest');
993+
obj.set('data', { key: 'value' });
994+
await obj.save();
995+
996+
const response = await request({
997+
method: 'GET',
998+
url: 'http://localhost:8378/1/classes/InjectionTest',
999+
headers,
1000+
qs: {
1001+
order: 'data.key',
1002+
},
1003+
});
1004+
expect(response.status).toBe(200);
1005+
});
1006+
1007+
it('allows valid dot-notation with special characters in sub-field', async () => {
1008+
const obj = new Parse.Object('InjectionTest');
1009+
obj.set('data', { 'my-field': 'value' });
1010+
await obj.save();
1011+
1012+
const response = await request({
1013+
method: 'GET',
1014+
url: 'http://localhost:8378/1/classes/InjectionTest',
1015+
headers,
1016+
qs: {
1017+
order: 'data.my-field',
1018+
},
1019+
});
1020+
expect(response.status).toBe(200);
1021+
});
1022+
});
1023+
8631024
describe('(GHSA-3jmq-rrxf-gqrg) Stored XSS via file serving', () => {
8641025
it('sets X-Content-Type-Options: nosniff on file GET response', async () => {
8651026
const file = new Parse.File('hello.txt', [1, 2, 3], 'text/plain');

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,12 @@ const handleDotFields = object => {
211211
const transformDotFieldToComponents = fieldName => {
212212
return fieldName.split('.').map((cmpt, index) => {
213213
if (index === 0) {
214-
return `"${cmpt}"`;
214+
return `"${cmpt.replace(/"/g, '""')}"`;
215215
}
216216
if (isArrayIndex(cmpt)) {
217217
return Number(cmpt);
218218
} else {
219-
return `'${cmpt}'`;
219+
return `'${cmpt.replace(/'/g, "''")}'`;
220220
}
221221
});
222222
};

0 commit comments

Comments
 (0)