Summary
| Attribute |
Value |
| Vendor / Org |
kornelski |
| Product |
http-cache-semantics |
| Component |
satisfiesWithoutRevalidation / maxAge (index.js) |
| Affected Versions |
<= 4.1.1 (present on main) |
| Severity |
High |
| CVSS 3.1 Score |
7.5 (High) |
| CVSS 3.1 Vector |
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| CWE |
CWE-525 (Use of Web Browser Cache Containing Sensitive Information) / CWE-639 |
| Affected File |
index.js:250-259, index.js:396-403 |
Description
A max-stale handling flaw in satisfiesWithoutRevalidation of kornelski/http-cache-semantics <= 4.1.1 allows an unauthenticated remote client to be served a shared-cache entry the library had deliberately marked non-reusable, including another user's Set-Cookie, by sending a single Cache-Control: max-stale request directive. The library sets maxAge() to 0 for a shared response that carries Set-Cookie without public, and for a proxy-revalidate response, specifically to stop that content being reused across users. The stale-serving branch only checks must-revalidate before honoring max-stale, so it re-serves those zero-lifetime entries anyway. An attacker who requests the same URL with a large max-stale receives the cached response body and headers that were computed for a different user.
Impact
In a shared/proxy cache built on this library, any unauthenticated client can retrieve a cached response the library had flagged as non-shareable, including a Set-Cookie header that establishes another user's session. This is a cross-user confidentiality break driven entirely by an attacker-controlled request header. The exposure requires the shared cache to retain the entry and consult satisfiesWithoutRevalidation at read time, which is the documented usage pattern in the README.
Remediation
Recommended Fix
When maxAge() is zeroed for a security reason (shared Set-Cookie without public, proxy-revalidate, no-cache), the stale-serving branch must not be reachable via max-stale. Track those prohibitions separately from ordinary expiry and gate the allowsStale branch on them, e.g. refuse max-stale when this._isShared && this._resHeaders['set-cookie'] && !this._rescc.public, or when this._rescc['proxy-revalidate']. This keeps the existing maxAge()===0 guarantee intact for consumers who use satisfiesWithoutRevalidation as documented.
Workaround
Consumers can strip Set-Cookie before caching and refuse to store proxy-revalidate responses, or ignore client max-stale in a shared deployment.
References
Summary
satisfiesWithoutRevalidation/maxAge(index.js)<= 4.1.1(present onmain)CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:Nindex.js:250-259,index.js:396-403Description
A
max-stalehandling flaw insatisfiesWithoutRevalidationof kornelski/http-cache-semantics<= 4.1.1allows an unauthenticated remote client to be served a shared-cache entry the library had deliberately marked non-reusable, including another user'sSet-Cookie, by sending a singleCache-Control: max-stalerequest directive. The library setsmaxAge()to0for a shared response that carriesSet-Cookiewithoutpublic, and for aproxy-revalidateresponse, specifically to stop that content being reused across users. The stale-serving branch only checksmust-revalidatebefore honoringmax-stale, so it re-serves those zero-lifetime entries anyway. An attacker who requests the same URL with a largemax-stalereceives the cached response body and headers that were computed for a different user.Impact
In a shared/proxy cache built on this library, any unauthenticated client can retrieve a cached response the library had flagged as non-shareable, including a
Set-Cookieheader that establishes another user's session. This is a cross-user confidentiality break driven entirely by an attacker-controlled request header. The exposure requires the shared cache to retain the entry and consultsatisfiesWithoutRevalidationat read time, which is the documented usage pattern in the README.Remediation
Recommended Fix
When
maxAge()is zeroed for a security reason (sharedSet-Cookiewithoutpublic,proxy-revalidate,no-cache), the stale-serving branch must not be reachable viamax-stale. Track those prohibitions separately from ordinary expiry and gate theallowsStalebranch on them, e.g. refusemax-stalewhenthis._isShared && this._resHeaders['set-cookie'] && !this._rescc.public, or whenthis._rescc['proxy-revalidate']. This keeps the existingmaxAge()===0guarantee intact for consumers who usesatisfiesWithoutRevalidationas documented.Workaround
Consumers can strip
Set-Cookiebefore caching and refuse to storeproxy-revalidateresponses, or ignore clientmax-stalein a shared deployment.References