Summary
| Attribute |
Value |
| Vendor / Org |
kornelski |
| Product |
http-cache-semantics |
| Component |
_varyMatches (index.js) |
| Affected Versions |
<= 4.1.1 (present on main) |
| Severity |
Medium |
| CVSS 3.1 Score |
5.9 (Medium) |
| CVSS 3.1 Vector |
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N |
| CWE |
CWE-436 (Interpretation Conflict) |
| Affected File |
index.js:293-304 |
Description
An interpretation conflict in _varyMatches of kornelski/http-cache-semantics <= 4.1.1 lets a stored response the origin marked Vary: * (never reuse) be served to a different client whenever the header is written in a whitespace or trailing-comma form such as "* ", " *", or "*,", or when the Vary field name resolves through the JavaScript prototype chain (e.g. Vary: constructor). The Vary: * block is an exact string equality check (this._resHeaders.vary === '*'), so any value that is semantically * but not byte-identical falls through to per-field matching. For the whitespace/comma forms the split produces a * (or empty) field whose header value is undefined on both sides and therefore compares equal; for constructor both req.headers and the stored map inherit the same prototype value and also compare equal. In each case the vary check returns true and the origin's "do not reuse" instruction is ignored.
Impact
A shared cache using this library can serve a response the origin explicitly marked Vary: * (or Vary: <field>) to a client for whom it was not computed, when the origin uses a non-canonical whitespace form or a header name that collides with an Object.prototype member. Vary: * is the origin's strongest "never reuse this across requests" signal, commonly emitted for per-user content. The result is cross-client response disclosure decided by attacker-independent but origin-reachable header formatting.
Remediation
Recommended Fix
Normalize before the * test: split and trim first, then treat any field equal to * as the wildcard (if (fields.includes('*')) return false;). Guard the per-field lookup with Object.prototype.hasOwnProperty.call(req.headers, name) on both operands, or use a null-prototype map, so inherited names cannot match vacuously. This restores the documented Vary semantics the library already claims to implement ("It's aware of many tricky details such as the Vary header").
Workaround
Consumers can canonicalize Vary to a bare * before storing, and reject responses whose Vary field names are not simple tokens.
References
Summary
_varyMatches(index.js)<= 4.1.1(present onmain)CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:Nindex.js:293-304Description
An interpretation conflict in
_varyMatchesof kornelski/http-cache-semantics<= 4.1.1lets a stored response the origin markedVary: *(never reuse) be served to a different client whenever the header is written in a whitespace or trailing-comma form such as"* "," *", or"*,", or when theVaryfield name resolves through the JavaScript prototype chain (e.g.Vary: constructor). TheVary: *block is an exact string equality check (this._resHeaders.vary === '*'), so any value that is semantically*but not byte-identical falls through to per-field matching. For the whitespace/comma forms the split produces a*(or empty) field whose header value isundefinedon both sides and therefore compares equal; forconstructorbothreq.headersand the stored map inherit the same prototype value and also compare equal. In each case the vary check returnstrueand the origin's "do not reuse" instruction is ignored.Impact
A shared cache using this library can serve a response the origin explicitly marked
Vary: *(orVary: <field>) to a client for whom it was not computed, when the origin uses a non-canonical whitespace form or a header name that collides with anObject.prototypemember.Vary: *is the origin's strongest "never reuse this across requests" signal, commonly emitted for per-user content. The result is cross-client response disclosure decided by attacker-independent but origin-reachable header formatting.Remediation
Recommended Fix
Normalize before the
*test: split and trim first, then treat any field equal to*as the wildcard (if (fields.includes('*')) return false;). Guard the per-field lookup withObject.prototype.hasOwnProperty.call(req.headers, name)on both operands, or use anull-prototype map, so inherited names cannot match vacuously. This restores the documentedVarysemantics the library already claims to implement ("It's aware of many tricky details such as theVaryheader").Workaround
Consumers can canonicalize
Varyto a bare*before storing, and reject responses whoseVaryfield names are not simple tokens.References