axios-cache-interceptor Vulnerable to Cache Poisoning via Ignored HTTP Vary Header
Description
Axios Cache Interceptor is a cache interceptor for axios. Prior to version 1.11.1, when a server calls an upstream service using different auth tokens, axios-cache-interceptor returns incorrect cached responses, leading to authorization bypass. The cache key is generated only from the URL, ignoring request headers like Authorization. When the server responds with Vary: Authorization (indicating the response varies by auth token), the library ignores this, causing all requests to share the same cache regardless of authorization. Server-side applications (APIs, proxies, backend services) that use axios-cache-interceptor to cache requests to upstream services, handle requests from multiple users with different auth tokens, and upstream services replies on Vary to differentiate caches are affected. Browser/client-side applications (single user per browser session) are not affected. Services using different auth tokens to call upstream services will return incorrect cached data, bypassing authorization checks and leaking user data across different authenticated sessions. After v1.11.1, automatic Vary header support is now enabled by default. When server responds with Vary: Authorization, cache keys now include the authorization header value. Each user gets their own cache.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
axios-cache-interceptornpm | < 1.11.1 | 1.11.1 |
Affected products
1- Range: 1.6.1, v0.0.1, v0.0.2, …
Patches
149a808059dfcFoolproof request headers casing (#1157)
7 files changed · +59 −42
src/cache/axios.ts+2 −2 modified@@ -2,8 +2,8 @@ import type { AxiosInstance, AxiosInterceptorManager, AxiosRequestConfig, + AxiosRequestHeaders, AxiosResponse, - AxiosResponseHeaders, InternalAxiosRequestConfig } from 'axios'; import type { CacheInstance, CacheProperties } from './cache.js'; @@ -94,7 +94,7 @@ export interface CacheRequestConfig<R = any, D = any> extends AxiosRequestConfig /** Cached version of type {@link InternalAxiosRequestConfig} */ export interface InternalCacheRequestConfig<R = any, D = any> extends CacheRequestConfig<R, D> { - headers: AxiosResponseHeaders; + headers: AxiosRequestHeaders; } /**
src/header/extract.ts+6 −4 modified@@ -1,3 +1,5 @@ +import type { AxiosRequestHeaders, AxiosResponseHeaders } from 'axios'; + /** * Extracts specified header values from request headers. * Generic utility for extracting a subset of headers. @@ -7,13 +9,13 @@ * @returns Object with extracted header values */ export function extractHeaders( - requestHeaders: Record<string, any>, + requestHeaders: AxiosRequestHeaders | AxiosResponseHeaders, headerNames: string[] -): Record<string, string> { - const result: Record<string, string> = {}; +): Record<string, string | undefined> { + const result: Record<string, string | undefined> = {}; for (const name of headerNames) { - result[name] = String(requestHeaders[name.toLowerCase()] || ''); + result[name] = requestHeaders.get(name)?.toString(); } return result;
src/interceptors/request.ts+9 −10 modified@@ -6,12 +6,7 @@ import { Header } from '../header/headers.js'; import type { CachedResponse, LoadingStorageValue } from '../storage/types.js'; import { regexOrStringMatch } from '../util/cache-predicate.js'; import type { RequestInterceptor } from './build.js'; -import { - type ConfigWithCache, - createValidateStatus, - isMethodIn, - updateStaleRequest -} from './util.js'; +import { createValidateStatus, isMethodIn, updateStaleRequest } from './util.js'; export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInterceptor { const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => { @@ -110,9 +105,13 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInt // shouldn't be cached an therefore neither in the browser. // https://stackoverflow.com/a/2068407 if (config.cache.cacheTakeover) { - config.headers[Header.CacheControl] ??= 'no-cache, no-store, must-revalidate, max-age=0'; - config.headers[Header.Pragma] ??= 'no-cache'; - config.headers[Header.Expires] ??= '0'; + config.headers.set( + Header.CacheControl, + 'no-cache, no-store, must-revalidate, max-age=0', + false + ); + config.headers.set(Header.Pragma, 'no-cache', false); + config.headers.set(Header.Expires, '0', false); } if (!isMethodIn(config.method, config.cache.methods)) { @@ -232,7 +231,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInt // The override option is meant to bypass cache and get fresh data, not revalidate existing cache. // Adding conditional headers would cause the server to return 304 Not Modified instead of fresh data. if ((cache.state === 'stale' || cache.state === 'must-revalidate') && !overrideCache) { - updateStaleRequest(cache, config as ConfigWithCache<unknown>); + updateStaleRequest(cache, { ...config, cache: config.cache }); if (__ACI_DEV__) { axios.debug({
src/interceptors/util.ts+14 −11 modified@@ -1,5 +1,9 @@ import type { Method } from 'axios'; -import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios.js'; +import type { + CacheAxiosResponse, + CacheRequestConfig, + InternalCacheRequestConfig +} from '../cache/axios.js'; import type { CacheProperties } from '../cache/cache.js'; import { Header } from '../header/headers.js'; import type { @@ -29,7 +33,7 @@ export function isMethodIn( return methodList.some((method) => method === requestMethod); } -export interface ConfigWithCache<D> extends CacheRequestConfig<unknown, D> { +export interface ConfigWithCache<D> extends InternalCacheRequestConfig<unknown, D> { cache: Partial<CacheProperties<unknown, D>>; } @@ -41,25 +45,24 @@ export function updateStaleRequest<D>( cache: StaleStorageValue | MustRevalidateStorageValue, config: ConfigWithCache<D> ): void { - config.headers ||= {}; - const { etag, modifiedSince } = config.cache; if (etag) { - const etagValue = etag === true ? (cache.data?.headers[Header.ETag] as unknown) : etag; + const etagValue = etag === true ? cache.data?.headers[Header.ETag] : etag; if (etagValue) { - config.headers[Header.IfNoneMatch] = etagValue; + config.headers.set(Header.IfNoneMatch, etagValue); } } if (modifiedSince) { - config.headers[Header.IfModifiedSince] = + config.headers.set( + Header.IfModifiedSince, + // If last-modified is not present, use the createdAt timestamp modifiedSince === true - ? // If last-modified is not present, use the createdAt timestamp - (cache.data.headers[Header.LastModified] as unknown) || - new Date(cache.createdAt).toUTCString() - : modifiedSince.toUTCString(); + ? cache.data.headers[Header.LastModified] || new Date(cache.createdAt).toUTCString() + : modifiedSince.toUTCString() + ); } }
src/storage/types.ts+1 −1 modified@@ -17,7 +17,7 @@ export interface CachedResponseMeta { * vary: { authorization: 'Bearer X' } * } */ - vary?: Record<string, string>; + vary?: Record<string, string | undefined>; } export interface CachedResponse {
test/interceptors/request.test.ts+17 −4 modified@@ -377,10 +377,23 @@ describe('Request Interceptor', () => { cache: { cacheTakeover: false } }); - const headers2 = req2.request.config.headers as Record<string, string>; - assert.equal(headers2[Header.CacheControl], undefined); - assert.equal(headers2[Header.Pragma], undefined); - assert.equal(headers2[Header.Expires], undefined); + const headers2 = req2.request.config.headers; + assert.equal(headers2.get(Header.CacheControl), undefined); + assert.equal(headers2.get(Header.Pragma), undefined); + assert.equal(headers2.get(Header.Expires), undefined); + + const req3 = await axios.get('url3', { + cache: { cacheTakeover: true }, + headers: { PRAGma: 'my-custom-value' } + }); + + const headers3 = req3.request.config.headers; + assert.equal( + headers3.get(Header.CacheControl), + 'no-cache, no-store, must-revalidate, max-age=0' + ); + assert.equal(headers3.get(Header.Pragma), 'my-custom-value'); + assert.equal(headers3.get(Header.Expires), '0'); }); it('ensure cached data is not transformed', async () => {
test/interceptors/vary.test.ts+10 −10 modified@@ -36,11 +36,11 @@ describe('Vary Header Support', () => { }); const resp2 = await axios.get('url', { - headers: { authorization: 'Bearer A', 'accept-language': 'en' } + headers: { authorization: 'Bearer A', 'Accept-Language': 'en' } }); const resp3 = await axios.get('url', { - headers: { authorization: 'Bearer A', 'accept-language': 'fr' } + headers: { authorization: 'Bearer A', 'ACCEPT-LANGUAGE': 'fr' } }); assert.equal(resp1.cached, false); @@ -59,22 +59,22 @@ describe('Vary Header Support', () => { return { user: Math.random(), call: networkCallCount, timestamp: Date.now() }; }); - // 9 concurrent requests: 3 variations, 3 requests each + // 9 concurrent requests: 3 variations, 3 requests each (in different casing) const requests = [ // 3 with Bearer A axios.get('url', { headers: { authorization: 'Bearer A' } }), - axios.get('url', { headers: { authorization: 'Bearer A' } }), + axios.get('url', { headers: { auTHORIZAtion: 'Bearer A' } }), axios.get('url', { headers: { authorization: 'Bearer A' } }), // 3 with Bearer B - axios.get('url', { headers: { authorization: 'Bearer B' } }), - axios.get('url', { headers: { authorization: 'Bearer B' } }), - axios.get('url', { headers: { authorization: 'Bearer B' } }), + axios.get('url', { headers: { Authorization: 'Bearer B' } }), + axios.get('url', { headers: { AUTHORIZATION: 'Bearer B' } }), + axios.get('url', { headers: { auTHOrization: 'Bearer B' } }), // 3 with Bearer C - axios.get('url', { headers: { authorization: 'Bearer C' } }), - axios.get('url', { headers: { authorization: 'Bearer C' } }), - axios.get('url', { headers: { authorization: 'Bearer C' } }) + axios.get('url', { headers: { authOrization: 'Bearer C' } }), + axios.get('url', { headers: { authorIZAtion: 'Bearer C' } }), + axios.get('url', { headers: { authorizATion: 'Bearer C' } }) ]; const responses = await Promise.all(requests);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-x4m5-4cw8-vc44ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-69202ghsaADVISORY
- github.com/arthurfiorette/axios-cache-interceptor/commit/49a808059dfc081b9cc23d48f243d55dfce15f01ghsax_refsource_MISCWEB
- github.com/arthurfiorette/axios-cache-interceptor/security/advisories/GHSA-x4m5-4cw8-vc44ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.