Skip to content

Commit 528b90d

Browse files
Disable prototype poisoning option (#1414) (#1420)
* Introduce disablePrototypePoisoningProtection option * Updated test * Updated docs * Fix bundler test Co-authored-by: Tomas Della Vedova <[email protected]>
1 parent 9fe0885 commit 528b90d

File tree

9 files changed

+166
-31
lines changed

9 files changed

+166
-31
lines changed

docs/basic-config.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,8 @@ const client = new Client({
244244
})
245245
----
246246

247+
|`disablePrototypePoisoningProtection`
248+
|`boolean`, `'proto'`, `'constructor'` - By the default the client will protect you against prototype poisoning attacks. Read https://web.archive.org/web/20200319091159/https://hueniverse.com/square-brackets-are-the-enemy-ff5b9fd8a3e8?gi=184a27ee2a08[this article] to learn more. If needed you can disable prototype poisoning protection entirely or one of the two checks. Read the `secure-json-parse` https://github.com/fastify/secure-json-parse[documentation] to learn more. +
249+
_Default:_ `false`
250+
247251
|===

index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ interface ClientOptions {
114114
// TODO: remove username and password here in 8
115115
username?: string;
116116
password?: string;
117-
}
117+
};
118+
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
118119
}
119120

120121
declare class Client {

index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ class Client extends ESAPI {
123123
opaqueIdPrefix: null,
124124
context: null,
125125
proxy: null,
126-
enableMetaHeader: true
126+
enableMetaHeader: true,
127+
disablePrototypePoisoningProtection: false
127128
}, opts)
128129

129130
this[kInitialOptions] = options
@@ -140,7 +141,9 @@ class Client extends ESAPI {
140141
this[kEventEmitter] = options[kChild].eventEmitter
141142
} else {
142143
this[kEventEmitter] = new EventEmitter()
143-
this.serializer = new options.Serializer()
144+
this.serializer = new options.Serializer({
145+
disablePrototypePoisoningProtection: options.disablePrototypePoisoningProtection
146+
})
144147
this.connectionPool = new options.ConnectionPool({
145148
pingTimeout: options.pingTimeout,
146149
resurrectStrategy: options.resurrectStrategy,

lib/Helpers.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ class Helpers {
421421
*/
422422
bulk (options, reqOptions = {}) {
423423
const client = this[kClient]
424-
const { serialize, deserialize } = client.serializer
424+
const { serializer } = client
425425
if (this[kMetaHeader] !== null) {
426426
reqOptions.headers = reqOptions.headers || {}
427427
reqOptions.headers['x-elastic-client-meta'] = this[kMetaHeader] + ',h=bp'
@@ -505,19 +505,19 @@ class Helpers {
505505
? Object.keys(action[0])[0]
506506
: Object.keys(action)[0]
507507
if (operation === 'index' || operation === 'create') {
508-
actionBody = serialize(action)
509-
payloadBody = typeof chunk === 'string' ? chunk : serialize(chunk)
508+
actionBody = serializer.serialize(action)
509+
payloadBody = typeof chunk === 'string' ? chunk : serializer.serialize(chunk)
510510
chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
511511
bulkBody.push(actionBody, payloadBody)
512512
} else if (operation === 'update') {
513-
actionBody = serialize(action[0])
513+
actionBody = serializer.serialize(action[0])
514514
payloadBody = typeof chunk === 'string'
515515
? `{"doc":${chunk}}`
516-
: serialize({ doc: chunk, ...action[1] })
516+
: serializer.serialize({ doc: chunk, ...action[1] })
517517
chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
518518
bulkBody.push(actionBody, payloadBody)
519519
} else if (operation === 'delete') {
520-
actionBody = serialize(action)
520+
actionBody = serializer.serialize(action)
521521
chunkBytes += Buffer.byteLength(actionBody)
522522
bulkBody.push(actionBody)
523523
} else {
@@ -669,13 +669,13 @@ class Helpers {
669669
return
670670
}
671671
for (let i = 0, len = bulkBody.length; i < len; i = i + 2) {
672-
const operation = Object.keys(deserialize(bulkBody[i]))[0]
672+
const operation = Object.keys(serializer.deserialize(bulkBody[i]))[0]
673673
onDrop({
674674
status: 429,
675675
error: null,
676-
operation: deserialize(bulkBody[i]),
676+
operation: serializer.deserialize(bulkBody[i]),
677677
document: operation !== 'delete'
678-
? deserialize(bulkBody[i + 1])
678+
? serializer.deserialize(bulkBody[i + 1])
679679
/* istanbul ignore next */
680680
: null,
681681
retried: isRetrying
@@ -716,9 +716,9 @@ class Helpers {
716716
onDrop({
717717
status: status,
718718
error: action[operation].error,
719-
operation: deserialize(bulkBody[indexSlice]),
719+
operation: serializer.deserialize(bulkBody[indexSlice]),
720720
document: operation !== 'delete'
721-
? deserialize(bulkBody[indexSlice + 1])
721+
? serializer.deserialize(bulkBody[indexSlice + 1])
722722
: null,
723723
retried: isRetrying
724724
})

lib/Serializer.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
* under the License.
1818
*/
1919

20+
export interface SerializerOptions {
21+
disablePrototypePoisoningProtection: boolean | 'proto' | 'constructor'
22+
}
23+
2024
export default class Serializer {
25+
constructor (opts?: SerializerOptions)
2126
serialize(object: any): string;
2227
deserialize(json: string): any;
2328
ndserialize(array: any[]): string;

lib/Serializer.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ const { stringify } = require('querystring')
2323
const debug = require('debug')('elasticsearch')
2424
const sjson = require('secure-json-parse')
2525
const { SerializationError, DeserializationError } = require('./errors')
26+
const kJsonOptions = Symbol('secure json parse options')
2627

2728
class Serializer {
29+
constructor (opts = {}) {
30+
const disable = opts.disablePrototypePoisoningProtection
31+
this[kJsonOptions] = {
32+
protoAction: disable === true || disable === 'proto' ? 'ignore' : 'error',
33+
constructorAction: disable === true || disable === 'constructor' ? 'ignore' : 'error'
34+
}
35+
}
36+
2837
serialize (object) {
2938
debug('Serializing', object)
3039
let json
@@ -40,7 +49,7 @@ class Serializer {
4049
debug('Deserializing', json)
4150
let object
4251
try {
43-
object = sjson.parse(json)
52+
object = sjson.parse(json, this[kJsonOptions])
4453
} catch (err) {
4554
throw new DeserializationError(err.message, json)
4655
}

test/types/client-options.test-d.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -557,22 +557,6 @@ expectError<errors.ConfigurationError>(
557557
)
558558
}
559559

560-
{
561-
class CustomSerializer {
562-
deserialize (str: string) {
563-
return JSON.parse(str)
564-
}
565-
}
566-
567-
expectError<errors.ConfigurationError>(
568-
// @ts-expect-error
569-
new Client({
570-
node: 'http://localhost:9200',
571-
Serializer: CustomSerializer
572-
})
573-
)
574-
}
575-
576560
/**
577561
* `Connection` option
578562
*/

test/unit/client.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,3 +1364,60 @@ test('Meta header disabled', t => {
13641364
t.error(err)
13651365
})
13661366
})
1367+
1368+
test('Prototype poisoning protection enabled by default', t => {
1369+
t.plan(1)
1370+
1371+
class MockConnection extends Connection {
1372+
request (params, callback) {
1373+
const stream = intoStream('{"__proto__":{"foo":"bar"}}')
1374+
stream.statusCode = 200
1375+
stream.headers = {
1376+
'content-type': 'application/json;utf=8',
1377+
'content-length': '27',
1378+
connection: 'keep-alive',
1379+
date: new Date().toISOString()
1380+
}
1381+
process.nextTick(callback, null, stream)
1382+
return { abort () {} }
1383+
}
1384+
}
1385+
1386+
const client = new Client({
1387+
node: 'http://localhost:9200',
1388+
Connection: MockConnection
1389+
})
1390+
1391+
client.info((err, result) => {
1392+
t.true(err instanceof errors.DeserializationError)
1393+
})
1394+
})
1395+
1396+
test('Disable prototype poisoning protection', t => {
1397+
t.plan(1)
1398+
1399+
class MockConnection extends Connection {
1400+
request (params, callback) {
1401+
const stream = intoStream('{"__proto__":{"foo":"bar"}}')
1402+
stream.statusCode = 200
1403+
stream.headers = {
1404+
'content-type': 'application/json;utf=8',
1405+
'content-length': '27',
1406+
connection: 'keep-alive',
1407+
date: new Date().toISOString()
1408+
}
1409+
process.nextTick(callback, null, stream)
1410+
return { abort () {} }
1411+
}
1412+
}
1413+
1414+
const client = new Client({
1415+
node: 'http://localhost:9200',
1416+
Connection: MockConnection,
1417+
disablePrototypePoisoningProtection: true
1418+
})
1419+
1420+
client.info((err, result) => {
1421+
t.error(err)
1422+
})
1423+
})

test/unit/serializer.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,75 @@ test('DeserializationError', t => {
157157
t.ok(err instanceof DeserializationError)
158158
}
159159
})
160+
161+
test('prototype poisoning protection', t => {
162+
t.plan(2)
163+
const s = new Serializer()
164+
try {
165+
s.deserialize('{"__proto__":{"foo":"bar"}}')
166+
t.fail('Should fail')
167+
} catch (err) {
168+
t.ok(err instanceof DeserializationError)
169+
}
170+
171+
try {
172+
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
173+
t.fail('Should fail')
174+
} catch (err) {
175+
t.ok(err instanceof DeserializationError)
176+
}
177+
})
178+
179+
test('disable prototype poisoning protection', t => {
180+
t.plan(2)
181+
const s = new Serializer({ disablePrototypePoisoningProtection: true })
182+
try {
183+
s.deserialize('{"__proto__":{"foo":"bar"}}')
184+
t.pass('Should not fail')
185+
} catch (err) {
186+
t.fail(err)
187+
}
188+
189+
try {
190+
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
191+
t.pass('Should not fail')
192+
} catch (err) {
193+
t.fail(err)
194+
}
195+
})
196+
197+
test('disable prototype poisoning protection only for proto', t => {
198+
t.plan(2)
199+
const s = new Serializer({ disablePrototypePoisoningProtection: 'proto' })
200+
try {
201+
s.deserialize('{"__proto__":{"foo":"bar"}}')
202+
t.pass('Should not fail')
203+
} catch (err) {
204+
t.fail(err)
205+
}
206+
207+
try {
208+
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
209+
t.fail('Should fail')
210+
} catch (err) {
211+
t.ok(err instanceof DeserializationError)
212+
}
213+
})
214+
215+
test('disable prototype poisoning protection only for constructor', t => {
216+
t.plan(2)
217+
const s = new Serializer({ disablePrototypePoisoningProtection: 'constructor' })
218+
try {
219+
s.deserialize('{"__proto__":{"foo":"bar"}}')
220+
t.fail('Should fail')
221+
} catch (err) {
222+
t.ok(err instanceof DeserializationError)
223+
}
224+
225+
try {
226+
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
227+
t.pass('Should not fail')
228+
} catch (err) {
229+
t.fail(err)
230+
}
231+
})

0 commit comments

Comments
 (0)