fix(cache): keep a header named __proto__ out of the prototype chain - #5667
fix(cache): keep a header named __proto__ out of the prototype chain#5667luantaraschi wants to merge 1 commit into
Conversation
appendHeader probed the accumulator with headers[headerName], which returns Object.prototype for __proto__ instead of undefined. The header then took the append branch and assigned an array, and an array is accepted by the Object.prototype setter, so the accumulator was reprototyped rather than storing the header. The result inherited from that array, so a header named length resolved to a number, and the dropped header made two different requests produce the same deduplication key, the same collision class as nodejs#5012. Writes now go through a setHeader helper using Object.defineProperty, the guard parseHeaders already uses, and the probe uses Object.hasOwn.
|
While checking where else this shape appears, two more spots drop the same header. Neither is in this PR and I would rather ask than send them unprompted.
const current = headers[key]
headers[key] = current ? `${current}, ${val}` : valSo an h2 request carrying
The h2 one needs a real h2 round trip to test properly, and the mock one is a pure function so it is cheap. Happy to send either or both separately, in whatever order suits you, or to leave them if you would rather keep the surface small. |
|
Both are up now, since #5663 was approved with the same guard: #5668 for the snapshot mock and #5669 for h2. Each stands on its own, so merge or drop them independently of this one. The snapshot one turned out to have a second consequence I had not seen when I wrote the comment above: the dropped header makes two different requests share a snapshot key, so record mode collapses them and playback serves the wrong response. |
This relates to...
No open issue. Extends the collision class that #5012 covered for delimiters.
Rationale
appendHeaderinlib/util/cache.jsaccumulates onto a plain object and probes it by index:For
__proto__that probe returnsObject.prototype, notundefined, so the header takes the last branch and the accumulator is assigned an array. Unlike a string, an array is accepted by theObject.prototypesetter, so the assignment reprototypes the accumulator instead of storing the header.An own
__proto__key is not exotic.JSON.parseproduces one, which is how headers read from a config file or a request body arrive:Before:
Two problems from that one assignment. The header is gone, and the result now inherits from an array, so a lookup for a header named
lengthanswers2andat,map,keysand friends all resolve to functions.appendHeaderitself branches onheaders[headerName] === undefined, so a later header with one of those names takes the wrong branch.The lost header then reaches
makeDeduplicationKey, which rebuilds a plain object the same way, so two different requests produce one key:The comment above that function says the key format was reworked because different header sets could produce identical keys (#5012). This is the same failure, reached through the header name instead of the value.
There is no
Object.prototypepollution: the reprototyped object is the local accumulator, and the global prototype is untouched.Changes
A
setHeaderhelper writes the__proto__key withObject.defineProperty, matching the guardparseHeadersalready uses inlib/core/util.js.appendHeaderprobes withObject.hasOwnso an inherited name is not mistaken for an existing header, andmakeDeduplicationKeywrites through the same helper.I kept the accumulator a plain object rather than switching it to a null prototype: it is handed to user-supplied cache stores as part of the cache key, and dropping
Object.prototypefrom it would be a visible change for that code.After:
Two tests: one in
test/cache-interceptor/cache-utils.jsfor the normalization, next to the existing prototype-pollution test there, and one intest/interceptors/deduplicate.jsbeside the #5012 collision test.test/cache-interceptor77 passing,test/interceptors302 passing with 14 skipped as before,test/cache4 passing.Features
N/A
Bug Fixes
A header named
__proto__is no longer dropped from the normalized headers, no longer reprototypes the accumulator, and no longer makes two different requests share a deduplication key.Breaking Changes and Deprecations
None.
Status