diff --git a/benchmarks/core/parse-headers.mjs b/benchmarks/core/parse-headers.mjs index 439986da93d..a005b3b33b2 100644 --- a/benchmarks/core/parse-headers.mjs +++ b/benchmarks/core/parse-headers.mjs @@ -87,7 +87,7 @@ bench('noop', () => {}) bench('noop', () => {}) bench('noop', () => {}) -group('parseHeaders', () => { +group(() => { bench('parseHeaders', () => { for (let i = 0; i < headers.length; ++i) { parseHeaders(headers[i]) diff --git a/lib/core/util.js b/lib/core/util.js index db8dda53a81..767d586b93a 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -440,19 +440,39 @@ function parseHeaders (headers, obj) { const key = headerNameToString(headers[i]) let val = obj[key] - if (val) { - if (typeof val === 'string') { - val = [val] - obj[key] = val - } - val.push(headers[i + 1].toString('latin1')) - } else { - const headersValue = headers[i + 1] - if (typeof headersValue === 'string') { - obj[key] = headersValue + if (val !== undefined) { + if (!Object.hasOwn(obj, key)) { + const headersValue = typeof headers[i + 1] === 'string' + ? headers[i + 1] + : Array.isArray(headers[i + 1]) + ? headers[i + 1].map(x => x.toString('latin1')) + : headers[i + 1].toString('latin1') + + if (key === '__proto__') { + Object.defineProperty(obj, key, { + value: headersValue, + enumerable: true, + configurable: true, + writable: true + }) + } else { + obj[key] = headersValue + } } else { - obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('latin1')) : headersValue.toString('latin1') + if (typeof val === 'string') { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('latin1')) } + } else { + const headersValue = typeof headers[i + 1] === 'string' + ? headers[i + 1] + : Array.isArray(headers[i + 1]) + ? headers[i + 1].map(x => x.toString('latin1')) + : headers[i + 1].toString('latin1') + + obj[key] = headersValue } } diff --git a/test/prototype-headers.js b/test/prototype-headers.js new file mode 100644 index 00000000000..aa1bf15be26 --- /dev/null +++ b/test/prototype-headers.js @@ -0,0 +1,84 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { promisify } = require('node:util') +const net = require('node:net') +const { Client } = require('..') + +function createRawServer (response) { + return net.createServer((socket) => { + socket.once('data', () => { + socket.end(response) + }) + }) +} + +test('request handles response headers that shadow Object.prototype', async (t) => { + const server = createRawServer([ + 'HTTP/1.1 200 OK', + '__proto__: pwned', + 'constructor: built-in', + 'content-length: 2', + 'connection: close', + '', + 'OK' + ].join('\r\n')) + + t.after(() => { + server.closeAllConnections?.() + server.close() + }) + + await promisify(server.listen.bind(server))(0) + + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => client.close()) + + const { statusCode, headers, body } = await client.request({ + path: '/', + method: 'GET' + }) + + assert.strictEqual(statusCode, 200) + assert.strictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, 'pwned') + assert.strictEqual(Object.getOwnPropertyDescriptor(headers, 'constructor').value, 'built-in') + assert.strictEqual(await body.text(), 'OK') +}) + +test('request handles response trailers that shadow Object.prototype', async (t) => { + const server = createRawServer([ + 'HTTP/1.1 200 OK', + 'transfer-encoding: chunked', + 'trailer: __proto__, constructor', + 'connection: close', + '', + '2', + 'OK', + '0', + '__proto__: trailer', + 'constructor: built-in-trailer', + '', + '' + ].join('\r\n')) + + t.after(() => { + server.closeAllConnections?.() + server.close() + }) + + await promisify(server.listen.bind(server))(0) + + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => client.close()) + + const { statusCode, trailers, body } = await client.request({ + path: '/', + method: 'GET' + }) + + assert.strictEqual(statusCode, 200) + assert.strictEqual(await body.text(), 'OK') + assert.strictEqual(Object.getOwnPropertyDescriptor(trailers, '__proto__').value, 'trailer') + assert.strictEqual(Object.getOwnPropertyDescriptor(trailers, 'constructor').value, 'built-in-trailer') +})