HIGH cache poisoningadonisjsapi keys

Cache Poisoning in Adonisjs with Api Keys

Cache Poisoning in Adonisjs with Api Keys — how this specific combination creates or exposes the vulnerability

Cache poisoning in AdonisJS when API keys are handled via request headers or query parameters occurs when an attacker can influence cached responses in a way that causes a different downstream user to receive attacker-controlled data. Because HTTP caches (CDNs, reverse proxies, or in-memory caches) often use request attributes as cache keys, including headers and query parameters, an API key value that is improperly included in the cache key can cause the cache to store a response intended for one user and serve it to another.

In AdonisJS, if API keys are used for authentication but the application does not exclude them from caching logic, a cached response for user A’s key might be reused for user B’s request if the cache key is derived from the full URL plus the API key header. This can lead to information disclosure, where user A’s data appears to user B, and can amplify other issues like IDOR when cached representations contain private resources. For example, an endpoint like GET /invoices/:id that includes an api-key header in the cache key may cache a PDF invoice for user A with key KEY_A, and later serve that same cached PDF to user B if their request normalizes to the same cache key despite having a different api-key.

AdonisJS itself does not enforce any default cache control for route handlers or HTTP client responses; it is up to the developer to define what varies the cache key. If API keys are treated as part of the cache key or are otherwise reflected into cacheable output, you introduce a cache poisoning vector. An attacker who can guess or leak another user’s API key might probe whether shared caching infrastructure causes personalized responses to be cached and then retrieved across users. This becomes especially risky when responses contain sensitive payloads or links that should be user-specific.

Additional exposure can occur if cache invalidation is tied to URL patterns that do not incorporate the API key context, leaving tainted entries in the cache until TTL expiry. For instance, if an admin revokes a key or rotates keys, cached entries created with the old key may remain valid for other users if the cache key omitted the key or used a partial normalization that still collides across users. Therefore, when using API keys in AdonisJS, ensure that user-specific secrets are excluded from cache key construction and that cache rules are scoped to user context or that sensitive responses are marked no-store.

Api Keys-Specific Remediation in Adonisjs — concrete code fixes

To mitigate cache poisoning in AdonisJS when using API keys, explicitly control what participates in cache key generation and ensure sensitive authentication values are never used to derive cache identifiers. Below are concrete remediation patterns and code examples tailored to common AdonisJS setups, including route-level caching and HTTP client requests.

  • Exclude API key headers from cache keys: When you implement caching, do not include the API key header or query parameter in the cache key. Instead, use a normalized path plus selected, non-sensitive headers (e.g., content-type) and user context identifiers that do not reveal secrets.
  • Use per-user cache contexts: Scope cached data to the authenticated user’s identifier (e.g., user ID) rather than raw API key values, and enforce authorization checks on cache retrieval to ensure one user cannot read another’s cached data.
  • Mark sensitive responses as non-cacheable: For endpoints that return private data, set explicit cache-control headers so intermediaries do not store responses.

Example 1: Route-level caching in AdonisJS (using Cache) with API key exclusion

import Route from '@ioc:Adonis/Core/Route'
import Cache from '@ioc:Adonis/Addons/Cache'

Route.get('/invoices/:id', async ({ request, auth, response }) => {
  const user = await auth.authenticate()
  const invoiceId = request.param('id')

  // Build a cache key that excludes the API key header
  const apiKey = request.header('api-key')
  // Do NOT use apiKey in the cache key
  const cacheKey = `invoice:${invoiceId}:user:${user.id}`

  return Cache.getOrPut(cacheKey, async () => {
    const invoice = await Invoice.findOrFail(invoiceId)
    // Ensure the user is authorized to view this invoice
    if (invoice.userId !== user.id) {
      throw new Error('Unauthorized')
    }
    return invoice.serialize()
  })
})

Example 2: HTTP client requests with API keys while controlling caching behavior

import { Http } from '@adonisjs/core/build/standalone'

export async function fetchExternalData(url: string, apiKey: string) {
  // Use a transient client that does not rely on shared cache keys derived from the API key
  const client = Http.createClient({
    baseURL: url,
    timeout: 5000,
  })

  // Set the API key as a request header, but do not let it affect caching layers
  const response = await client.get('/data', {
    headers: {
      'api-key': apiKey,
      // Explicitly prevent caching of sensitive responses
      'Cache-Control': 'no-store',
    },
  })

  return response.json()
}

Example 3: Middleware to normalize cache behavior and strip sensitive headers

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { middleware } from '@adonisjs/core'

const cacheControlMiddleware = middleware(async (ctx: HttpContextContract, next) => {
  // Remove or ignore API key from cache-related decisions
  const { apiKey, ...safeHeaders } = ctx.request.headers()
  // Proceed with request; ensure downstream caching does not use apiKey
  await next()

  // Optionally set cache-control for sensitive responses
  if (ctx.response.status === 200 && ctx.request.url().startsWith('/private')) {
    ctx.response.header('Cache-Control', 'no-store')
  }
})

export default cacheControlMiddleware

Example 4: Scoped caching per user with authorization checks

import Route from '@ioc:Adonis/Core/Route'
import Cache from '@ioc:Adonis/Addons/Cache'

Route.get('/users/:userId/profile', async ({ params, auth, response }) => {
  const viewer = await auth.authenticate()
  const userId = params.id

  // Ensure the viewer can access this profile
  if (viewer.id !== Number(userId)) {
    return response.forbidden()
  }

  const cacheKey = `profile:user:${userId}`
  return Cache.getOrPut(cacheKey, async () => {
    const profile = await Profile.findOrFail(userId)
    return profile.serialize()
  })
})

Frequently Asked Questions

How does including an API key in a cache key cause cache poisoning in AdonisJS?
Including an API key in a cache key can cause cache poisoning because the cache may store a response for key A and later serve it to a request authenticated with key B if the cache key does not differentiate user context. This leads to one user receiving another user's data, often because the cache key incorporates the API key header or query parameter directly.
What is the best practice for using API keys in AdonisJS to avoid cache poisoning?
Exclude API keys from cache key construction, scope caches to user identifiers with proper authorization checks, and set Cache-Control: no-store on sensitive responses. Use per-user cache contexts and normalize URLs and headers to prevent leakage across users.