Skip to content

Commit 4a964a3

Browse files
authoredApr 9, 2024··
fix(security): improve results object creation (#2574)
Fixes a potential Prototype Pollution attack vulnerability reported by Vsevolod Kokorin (Slonser) of Solidlab
1 parent 71115d8 commit 4a964a3

File tree

6 files changed

+166
-12
lines changed

6 files changed

+166
-12
lines changed
 

‎.nycrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"reporter": ["text", "lcov", "cobertura"],
66
"statements": 88,
77
"branches": 84,
8-
"functions": 78,
8+
"functions": 77,
99
"lines": 88,
1010
"checkCoverage": true,
1111
"clean": true

‎lib/parsers/binary_parser.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,13 @@ function compile(fields, options, config) {
122122
if (options.rowsAsArray) {
123123
parserFn(`const result = new Array(${fields.length});`);
124124
} else {
125-
parserFn('const result = {};');
125+
parserFn('const result = Object.create(null);');
126+
parserFn(`Object.defineProperty(result, "constructor", {
127+
value: Object.create(null),
128+
writable: false,
129+
configurable: false,
130+
enumerable: false
131+
});`);
126132
}
127133

128134
// Global typeCast
@@ -154,7 +160,9 @@ function compile(fields, options, config) {
154160
)}]`;
155161
} else if (options.nestTables === true) {
156162
tableName = helpers.srcEscape(fields[i].table);
157-
parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`);
163+
parserFn(
164+
`if (!result[${tableName}]) result[${tableName}] = Object.create(null);`,
165+
);
158166
lvalue = `result[${tableName}][${fieldName}]`;
159167
} else if (options.rowsAsArray) {
160168
lvalue = `result[${i.toString(10)}]`;

‎lib/parsers/text_parser.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,6 @@ function compile(fields, options, config) {
111111

112112
const parserFn = genFunc();
113113

114-
/* eslint-disable no-trailing-spaces */
115-
/* eslint-disable no-spaced-func */
116-
/* eslint-disable no-unexpected-multiline */
117114
parserFn('(function () {')('return class TextRow {');
118115

119116
// constructor method
@@ -134,7 +131,13 @@ function compile(fields, options, config) {
134131
if (options.rowsAsArray) {
135132
parserFn(`const result = new Array(${fields.length});`);
136133
} else {
137-
parserFn('const result = {};');
134+
parserFn('const result = Object.create(null);');
135+
parserFn(`Object.defineProperty(result, "constructor", {
136+
value: Object.create(null),
137+
writable: false,
138+
configurable: false,
139+
enumerable: false
140+
});`);
138141
}
139142

140143
const resultTables = {};
@@ -146,7 +149,9 @@ function compile(fields, options, config) {
146149
}
147150
resultTablesArray = Object.keys(resultTables);
148151
for (let i = 0; i < resultTablesArray.length; i++) {
149-
parserFn(`result[${helpers.srcEscape(resultTablesArray[i])}] = {};`);
152+
parserFn(
153+
`result[${helpers.srcEscape(resultTablesArray[i])}] = Object.create(null);`,
154+
);
150155
}
151156
}
152157

@@ -191,10 +196,6 @@ function compile(fields, options, config) {
191196
parserFn('}');
192197
parserFn('};')('})()');
193198

194-
/* eslint-enable no-trailing-spaces */
195-
/* eslint-enable no-spaced-func */
196-
/* eslint-enable no-unexpected-multiline */
197-
198199
if (config.debug) {
199200
helpers.printDebugWithCode(
200201
'Compiled text protocol row parser',

‎test/common.test.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ exports.createConnection = function (args) {
105105
typeCast: args && args.typeCast,
106106
namedPlaceholders: args && args.namedPlaceholders,
107107
connectTimeout: args && args.connectTimeout,
108+
nestTables: args && args.nestTables,
108109
ssl: (args && args.ssl) ?? config.ssl,
109110
};
110111

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { test, describe, assert } from 'poku';
2+
import { createConnection, describeOptions } from '../../../common.test.cjs';
3+
4+
const connection = createConnection().promise();
5+
6+
describe('Binary Parser: Prototype Sanitization', describeOptions);
7+
8+
Promise.all([
9+
test(async () => {
10+
const expected = [{}];
11+
expected[0].test = 2;
12+
13+
const [results] = await connection.query('SELECT 1+1 AS `test`');
14+
15+
assert.notDeepStrictEqual(
16+
results,
17+
expected,
18+
`Ensure "results" doesn't contain a standard object ({})`,
19+
);
20+
}),
21+
test(async () => {
22+
const expected = [Object.create(null)];
23+
expected[0].test = 2;
24+
25+
const [results] = await connection.execute('SELECT 1+1 AS `test`');
26+
27+
assert.deepStrictEqual(results, expected, 'Ensure clean object "results"');
28+
assert.strictEqual(
29+
Object.getPrototypeOf(results[0]),
30+
null,
31+
'Ensure clean properties in results items',
32+
);
33+
assert.strictEqual(
34+
typeof results[0].toString,
35+
'undefined',
36+
'Re-check prototypes (manually) in results columns',
37+
);
38+
assert.strictEqual(
39+
typeof results[0].test.toString,
40+
'function',
41+
'Ensure that the end-user is able to use prototypes',
42+
);
43+
assert.strictEqual(
44+
results[0].test.toString(),
45+
'2',
46+
'Ensure that the end-user is able to use prototypes (manually): toString',
47+
);
48+
assert.strictEqual(
49+
results[0].test.toFixed(2),
50+
'2.00',
51+
'Ensure that the end-user is able to use prototypes (manually): toFixed',
52+
);
53+
54+
results[0].customProp = true;
55+
assert.strictEqual(
56+
results[0].customProp,
57+
true,
58+
'Ensure that the end-user is able to use custom props',
59+
);
60+
}),
61+
test(async () => {
62+
const [result] = await connection.execute('SET @1 = 1;');
63+
64+
assert.strictEqual(
65+
result.constructor.name,
66+
'ResultSetHeader',
67+
'Ensure constructor name in result object',
68+
);
69+
}),
70+
]).then(async () => {
71+
await connection.end();
72+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { test, describe, assert } from 'poku';
2+
import { createConnection, describeOptions } from '../../../common.test.cjs';
3+
4+
const connection = createConnection().promise();
5+
6+
describe('Text Parser: Prototype Sanitization', describeOptions);
7+
8+
Promise.all([
9+
test(async () => {
10+
const expected = [{}];
11+
expected[0].test = 2;
12+
13+
const [results] = await connection.query('SELECT 1+1 AS `test`');
14+
15+
assert.notDeepStrictEqual(
16+
results,
17+
expected,
18+
`Ensure "results" doesn't contain a standard object ({})`,
19+
);
20+
}),
21+
test(async () => {
22+
const expected = [Object.create(null)];
23+
expected[0].test = 2;
24+
25+
const [results] = await connection.query('SELECT 1+1 AS `test`');
26+
27+
assert.deepStrictEqual(results, expected, 'Ensure clean object "results"');
28+
assert.strictEqual(
29+
Object.getPrototypeOf(results[0]),
30+
null,
31+
'Ensure clean properties in results items',
32+
);
33+
assert.strictEqual(
34+
typeof results[0].toString,
35+
'undefined',
36+
'Re-check prototypes (manually) in results columns',
37+
);
38+
assert.strictEqual(
39+
typeof results[0].test.toString,
40+
'function',
41+
'Ensure that the end-user is able to use prototypes',
42+
);
43+
assert.strictEqual(
44+
results[0].test.toString(),
45+
'2',
46+
'Ensure that the end-user is able to use prototypes (manually): toString',
47+
);
48+
assert.strictEqual(
49+
results[0].test.toFixed(2),
50+
'2.00',
51+
'Ensure that the end-user is able to use prototypes (manually): toFixed',
52+
);
53+
54+
results[0].customProp = true;
55+
assert.strictEqual(
56+
results[0].customProp,
57+
true,
58+
'Ensure that the end-user is able to use custom props',
59+
);
60+
}),
61+
test(async () => {
62+
const [result] = await connection.query('SET @1 = 1;');
63+
64+
assert.strictEqual(
65+
result.constructor.name,
66+
'ResultSetHeader',
67+
'Ensure constructor name in result object',
68+
);
69+
}),
70+
]).then(async () => {
71+
await connection.end();
72+
});

0 commit comments

Comments
 (0)
Please sign in to comment.