Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/mock/snapshot-recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { writeFile, readFile, mkdir } = require('node:fs/promises')
const { dirname, resolve } = require('node:path')
const { setTimeout, clearTimeout } = require('node:timers')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils')
const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters, setHeader } = require('./snapshot-utils')

/**
* @typedef {Object} SnapshotRequestOptions
Expand Down Expand Up @@ -169,7 +169,7 @@ function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) {
if (!match.has(headerKey)) continue
}

filtered[headerKey] = value
setHeader(filtered, headerKey, value)
}

return filtered
Expand Down Expand Up @@ -198,7 +198,7 @@ function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) {
// Skip if in exclude list (for security)
if (excludeSet.has(headerKey)) continue

filtered[headerKey] = value
setHeader(filtered, headerKey, value)
}

return filtered
Expand Down
27 changes: 25 additions & 2 deletions lib/mock/snapshot-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ function isUrlExcludedFactory (excludePatterns = []) {
}
}

/**
* Writes a header onto a plain object without going through the
* Object.prototype `__proto__` setter, which would drop it. Same guard as
* parseHeaders in lib/core/util.js.
*
* @param {NormalizedHeaders} headers
* @param {string} name lowercased header name
* @param {string} value
*/
function setHeader (headers, name, value) {
if (name === '__proto__') {
Object.defineProperty(headers, name, {
configurable: true,
enumerable: true,
value,
writable: true
})
} else {
headers[name] = value
}
}

/**
* Normalizes headers for consistent comparison
*
Expand All @@ -116,7 +138,7 @@ function normalizeHeaders (headers) {
// Convert Buffers to strings if needed
const keyStr = Buffer.isBuffer(key) ? key.toString() : key
const valueStr = Buffer.isBuffer(value) ? value.toString() : value
normalizedHeaders[keyStr.toLowerCase()] = valueStr
setHeader(normalizedHeaders, keyStr.toLowerCase(), valueStr)
}
}
return normalizedHeaders
Expand All @@ -126,7 +148,7 @@ function normalizeHeaders (headers) {
if (headers && typeof headers === 'object') {
for (const [key, value] of Object.entries(headers)) {
if (key && typeof key === 'string') {
normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
setHeader(normalizedHeaders, key.toLowerCase(), Array.isArray(value) ? value.join(', ') : String(value))
}
}
}
Expand All @@ -153,6 +175,7 @@ module.exports = {
hashId,
isUndiciHeaders,
normalizeHeaders,
setHeader,
isUrlExcludedFactory,
validateSnapshotMode
}
28 changes: 28 additions & 0 deletions test/snapshot-recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,31 @@ test('SnapshotRecorder - redirect responses are stored correctly', (t) => {
assert.strictEqual(snapshot.responses[0].statusCode, 302, 'First response should be redirect')
assert.strictEqual(snapshot.responses[1].statusCode, 200, 'Second response should be final')
})

test('SnapshotRecorder - a header named __proto__ does not collide with its absence', (t) => {
const filters = createHeaderFilters({})

// JSON.parse is the everyday way an own `__proto__` key appears.
const withProto = formatRequestKey({
origin: 'https://example.com',
path: '/resource',
method: 'GET',
headers: JSON.parse('{"__proto__":"a","x-real":"same"}')
}, filters)

const withoutProto = formatRequestKey({
origin: 'https://example.com',
path: '/resource',
method: 'GET',
headers: { 'x-real': 'same' }
}, filters)

assert.strictEqual(
Object.getOwnPropertyDescriptor(withProto.headers, '__proto__')?.value,
'a'
)
assert.notStrictEqual(
createRequestHash(withProto),
createRequestHash(withoutProto)
)
})