Skip to content

fix(mock): keep a header named __proto__ in snapshots - #5668

Open
luantaraschi wants to merge 1 commit into
nodejs:mainfrom
luantaraschi:fix/snapshot-headers-proto
Open

fix(mock): keep a header named __proto__ in snapshots#5668
luantaraschi wants to merge 1 commit into
nodejs:mainfrom
luantaraschi:fix/snapshot-headers-proto

Conversation

@luantaraschi

Copy link
Copy Markdown

This relates to...

I raised this in a comment on #5667 and offered to send it separately. Sending it now that #5663 was approved, since it is the same guard on a subsystem where the consequence is different.

Rationale

Three places in the snapshot mock build a plain object keyed by header name and assign into it:

  • normalizeHeaders in lib/mock/snapshot-utils.js, both the array and the object branch
  • filterHeadersForMatching and filterHeadersForStorage in lib/mock/snapshot-recorder.js

__proto__ is a valid header name, and assigning it on a plain object reaches the Object.prototype setter instead of creating an own property. The value is a string, so the setter does nothing and the header is dropped.

It does not stop at a lost header. formatRequestKey feeds those headers to createRequestHash, which walks Object.keys(...).sort(), so two different requests produce one snapshot key:

const filters = createHeaderFilters({})
const a = formatRequestKey({ origin, path, method: 'GET',
  headers: JSON.parse('{"__proto__":"a","x-real":"same"}') }, filters)
const b = formatRequestKey({ origin, path, method: 'GET',
  headers: { 'x-real': 'same' } }, filters)

createRequestHash(a) === createRequestHash(b)   // true

In record mode the two collapse into one entry, and in playback the recorded response for one is served for the other. JSON.parse is the everyday way an own __proto__ key shows up, which is exactly how snapshots arrive from disk.

There is no Object.prototype pollution: the setter refuses a string, so the global prototype is untouched.

Changes

A setHeader helper in snapshot-utils.js writes the __proto__ key with Object.defineProperty, the guard parseHeaders already uses in lib/core/util.js. It is exported and used by all three writers.

The object stays plain rather than switching to a null prototype, because it is serialized into snapshot files and handed back to user code.

After the change the same pair hashes differently and the header survives:

headers stored: {"__proto__":"a","x-real":"same"}
hashes equal:   false

One test in test/snapshot-recorder.js, next to the existing formatRequestKey tests.

test/snapshot*.js 48 passing, test/mock*.js 339 passing, lint clean.

Features

N/A

Bug Fixes

A header named __proto__ is no longer dropped from a recorded snapshot and no longer makes two different requests share a snapshot key.

Breaking Changes and Deprecations

None.

Status

normalizeHeaders and both header filters assigned into a plain object, so the
__proto__ key reached the Object.prototype setter and the header was dropped.
formatRequestKey feeds those headers to createRequestHash, so two requests
differing only by that header produced the same snapshot key: one entry in
record mode, the wrong response in playback.

Writes now go through a setHeader helper using Object.defineProperty, the
guard parseHeaders already uses.
Copilot AI lite review requested due to automatic review settings August 8, 2026 02:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants