Disable prototype poisoning option (#1414)

* Introduce disablePrototypePoisoningProtection option

* Updated test

* Updated docs

* Fix bundler test
This commit is contained in:
Tomas Della Vedova
2021-03-15 08:51:45 +01:00
committed by GitHub
parent 36eaed6466
commit 6a30cd9955
9 changed files with 166 additions and 31 deletions

View File

@ -244,4 +244,8 @@ const client = new Client({
})
----
|`disablePrototypePoisoningProtection`
|`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. +
_Default:_ `false`
|===

3
index.d.ts vendored
View File

@ -114,7 +114,8 @@ interface ClientOptions {
// TODO: remove username and password here in 8
username?: string;
password?: string;
}
};
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
}
declare class Client {

View File

@ -123,7 +123,8 @@ class Client extends ESAPI {
opaqueIdPrefix: null,
context: null,
proxy: null,
enableMetaHeader: true
enableMetaHeader: true,
disablePrototypePoisoningProtection: false
}, opts)
this[kInitialOptions] = options
@ -140,7 +141,9 @@ class Client extends ESAPI {
this[kEventEmitter] = options[kChild].eventEmitter
} else {
this[kEventEmitter] = new EventEmitter()
this.serializer = new options.Serializer()
this.serializer = new options.Serializer({
disablePrototypePoisoningProtection: options.disablePrototypePoisoningProtection
})
this.connectionPool = new options.ConnectionPool({
pingTimeout: options.pingTimeout,
resurrectStrategy: options.resurrectStrategy,

View File

@ -421,7 +421,7 @@ class Helpers {
*/
bulk (options, reqOptions = {}) {
const client = this[kClient]
const { serialize, deserialize } = client.serializer
const { serializer } = client
if (this[kMetaHeader] !== null) {
reqOptions.headers = reqOptions.headers || {}
reqOptions.headers['x-elastic-client-meta'] = this[kMetaHeader] + ',h=bp'
@ -505,19 +505,19 @@ class Helpers {
? Object.keys(action[0])[0]
: Object.keys(action)[0]
if (operation === 'index' || operation === 'create') {
actionBody = serialize(action)
payloadBody = typeof chunk === 'string' ? chunk : serialize(chunk)
actionBody = serializer.serialize(action)
payloadBody = typeof chunk === 'string' ? chunk : serializer.serialize(chunk)
chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
bulkBody.push(actionBody, payloadBody)
} else if (operation === 'update') {
actionBody = serialize(action[0])
actionBody = serializer.serialize(action[0])
payloadBody = typeof chunk === 'string'
? `{"doc":${chunk}}`
: serialize({ doc: chunk, ...action[1] })
: serializer.serialize({ doc: chunk, ...action[1] })
chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
bulkBody.push(actionBody, payloadBody)
} else if (operation === 'delete') {
actionBody = serialize(action)
actionBody = serializer.serialize(action)
chunkBytes += Buffer.byteLength(actionBody)
bulkBody.push(actionBody)
} else {
@ -669,13 +669,13 @@ class Helpers {
return
}
for (let i = 0, len = bulkBody.length; i < len; i = i + 2) {
const operation = Object.keys(deserialize(bulkBody[i]))[0]
const operation = Object.keys(serializer.deserialize(bulkBody[i]))[0]
onDrop({
status: 429,
error: null,
operation: deserialize(bulkBody[i]),
operation: serializer.deserialize(bulkBody[i]),
document: operation !== 'delete'
? deserialize(bulkBody[i + 1])
? serializer.deserialize(bulkBody[i + 1])
/* istanbul ignore next */
: null,
retried: isRetrying
@ -716,9 +716,9 @@ class Helpers {
onDrop({
status: status,
error: action[operation].error,
operation: deserialize(bulkBody[indexSlice]),
operation: serializer.deserialize(bulkBody[indexSlice]),
document: operation !== 'delete'
? deserialize(bulkBody[indexSlice + 1])
? serializer.deserialize(bulkBody[indexSlice + 1])
: null,
retried: isRetrying
})

5
lib/Serializer.d.ts vendored
View File

@ -17,7 +17,12 @@
* under the License.
*/
export interface SerializerOptions {
disablePrototypePoisoningProtection: boolean | 'proto' | 'constructor'
}
export default class Serializer {
constructor (opts?: SerializerOptions)
serialize(object: any): string;
deserialize(json: string): any;
ndserialize(array: any[]): string;

View File

@ -23,8 +23,17 @@ const { stringify } = require('querystring')
const debug = require('debug')('elasticsearch')
const sjson = require('secure-json-parse')
const { SerializationError, DeserializationError } = require('./errors')
const kJsonOptions = Symbol('secure json parse options')
class Serializer {
constructor (opts = {}) {
const disable = opts.disablePrototypePoisoningProtection
this[kJsonOptions] = {
protoAction: disable === true || disable === 'proto' ? 'ignore' : 'error',
constructorAction: disable === true || disable === 'constructor' ? 'ignore' : 'error'
}
}
serialize (object) {
debug('Serializing', object)
let json
@ -40,7 +49,7 @@ class Serializer {
debug('Deserializing', json)
let object
try {
object = sjson.parse(json)
object = sjson.parse(json, this[kJsonOptions])
} catch (err) {
throw new DeserializationError(err.message, json)
}

View File

@ -557,22 +557,6 @@ expectError<errors.ConfigurationError>(
)
}
{
class CustomSerializer {
deserialize (str: string) {
return JSON.parse(str)
}
}
expectError<errors.ConfigurationError>(
// @ts-expect-error
new Client({
node: 'http://localhost:9200',
Serializer: CustomSerializer
})
)
}
/**
* `Connection` option
*/

View File

@ -1364,3 +1364,60 @@ test('Meta header disabled', t => {
t.error(err)
})
})
test('Prototype poisoning protection enabled by default', t => {
t.plan(1)
class MockConnection extends Connection {
request (params, callback) {
const stream = intoStream('{"__proto__":{"foo":"bar"}}')
stream.statusCode = 200
stream.headers = {
'content-type': 'application/json;utf=8',
'content-length': '27',
connection: 'keep-alive',
date: new Date().toISOString()
}
process.nextTick(callback, null, stream)
return { abort () {} }
}
}
const client = new Client({
node: 'http://localhost:9200',
Connection: MockConnection
})
client.info((err, result) => {
t.true(err instanceof errors.DeserializationError)
})
})
test('Disable prototype poisoning protection', t => {
t.plan(1)
class MockConnection extends Connection {
request (params, callback) {
const stream = intoStream('{"__proto__":{"foo":"bar"}}')
stream.statusCode = 200
stream.headers = {
'content-type': 'application/json;utf=8',
'content-length': '27',
connection: 'keep-alive',
date: new Date().toISOString()
}
process.nextTick(callback, null, stream)
return { abort () {} }
}
}
const client = new Client({
node: 'http://localhost:9200',
Connection: MockConnection,
disablePrototypePoisoningProtection: true
})
client.info((err, result) => {
t.error(err)
})
})

View File

@ -157,3 +157,75 @@ test('DeserializationError', t => {
t.ok(err instanceof DeserializationError)
}
})
test('prototype poisoning protection', t => {
t.plan(2)
const s = new Serializer()
try {
s.deserialize('{"__proto__":{"foo":"bar"}}')
t.fail('Should fail')
} catch (err) {
t.ok(err instanceof DeserializationError)
}
try {
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
t.fail('Should fail')
} catch (err) {
t.ok(err instanceof DeserializationError)
}
})
test('disable prototype poisoning protection', t => {
t.plan(2)
const s = new Serializer({ disablePrototypePoisoningProtection: true })
try {
s.deserialize('{"__proto__":{"foo":"bar"}}')
t.pass('Should not fail')
} catch (err) {
t.fail(err)
}
try {
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
t.pass('Should not fail')
} catch (err) {
t.fail(err)
}
})
test('disable prototype poisoning protection only for proto', t => {
t.plan(2)
const s = new Serializer({ disablePrototypePoisoningProtection: 'proto' })
try {
s.deserialize('{"__proto__":{"foo":"bar"}}')
t.pass('Should not fail')
} catch (err) {
t.fail(err)
}
try {
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
t.fail('Should fail')
} catch (err) {
t.ok(err instanceof DeserializationError)
}
})
test('disable prototype poisoning protection only for constructor', t => {
t.plan(2)
const s = new Serializer({ disablePrototypePoisoningProtection: 'constructor' })
try {
s.deserialize('{"__proto__":{"foo":"bar"}}')
t.fail('Should fail')
} catch (err) {
t.ok(err instanceof DeserializationError)
}
try {
s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
t.pass('Should not fail')
} catch (err) {
t.fail(err)
}
})