Cache Poisoning on Azure
How Cache Poisoning Manifests in Azure
Cache poisoning in Azure typically occurs when an attacker influences a value that participates in the cache key used by Azure services such as Azure CDN, Azure Front Door, Azure API Management, Azure App Service OutputCache, or Azure Redis Cache. If the cache key includes unvalidated client‑controlled data (e.g., the Host header, X-Forwarded-Host, or custom headers), a malicious request can cause the edge node to store a response under an attacker‑chosen key. Subsequent legitimate requests that match that poisoned key receive the attacker’s payload.
Common Azure‑specific patterns include:
- Azure CDN / Front Door: The default cache key incorporates the
Hostheader and the full URL. An attacker who can inject a arbitraryHostheader (via HTTP request smuggling or misconfigured backend) can cause the CDN to cache a response for a domain they control. - Azure API Management: When using the
cache-lookupandcache-storepolicies without avary-by-headerthat excludes unsafe headers, variations inUser-AgentorX-Original-URLcan create separate cache entries. - Azure App Service OutputCache: The
<outputCache>setting inweb.configcan be configured withvaryByHeader="*". If left default, any header (including attacker‑supplied ones) becomes part of the cache key. - Azure Redis Cache used directly by an application: Developers sometimes build a cache key by concatenating the request path with raw header values (e.g.,
string key = $"{Request.Path}_{Request.Headers["X-Theme"]}"). An attacker who controlsX-Themecan poison the cache.
Real‑world analogues include CVE‑2020-13942 (Apache HTTP Server host header injection) and CVE‑2021-21315 (GitLab Pages cache poisoning via X-Forwarded-Host). In Azure, the impact is amplified because the poisoned entry can be served globally from CDN edge nodes, affecting many users before the cache expires.
Azure-Specific Detection
Detecting cache poisoning starts with identifying which inputs contribute to the cache key and whether those inputs are properly validated or excluded. MiddleBrick’s unauthenticated black‑box scan includes checks for header‑based cache poisoning as part of its Input Validation and Data Exposure categories. When you submit a URL, MiddleBrick:
- Probes for reflective cache‑key headers by sending variations of
Host,X-Forwarded-Host,X-Host, and custom headers. - Examines response headers such as
Cache-Control,Vary, andAgeto infer whether a response was served from a cache. - Compares the payload of the baseline request with that of the mutated request; if the mutated request returns a different body that is subsequently cached (indicated by an
Ageheader > 0 on a follow‑up request), a finding is raised.
Example CLI usage:
middlebrick scan https://api.example.com/resource
Sample JSON finding (truncated for clarity):
{
"id": "CACHE-001",
"name": "Potential cache poisoning via Host header",
"severity": "medium",
"description": "The response varies with the Host header and is cached (Age: 12s). An attacker could poison the cache for arbitrary domains.",
"remediation": "Ensure the cache key excludes untrusted headers or use Azure Front Door rule engine to normalize the Host header."
}
In the Dashboard, the finding appears under the "Input Validation" section with a severity badge and a link to the remediation guidance. Repeated scans over time (available on the Pro plan) let you verify that the issue remains fixed after deploying a patch.
Azure-Specific Remediation
Remediation focuses on ensuring that the cache key is derived only from trusted, immutable components of the request (e.g., the request path and query string) and that any client‑controlled headers are either stripped or normalized before they influence caching.
Azure Front Door / CDN rule engine Create a rule set that rewrites or removes unsafe headers before they reach the origin:
# Front Door rules JSON (example)
{
"name": "Normalize Host",
"conditions": [
{
"name": "RequestHeader",
"parameters": {
"headerName": "Host",
"operator": "Any",
"values": ["*"]
}
}
],
"action": {
"name": "RequestHeader",
"parameters": {
"headerName": "Host",
"action": "Overwrite",
"value": "api.example.com"
}
}
}
This forces all requests to use a known host, eliminating host‑header‑based cache key variance.
Azure API Management policy
Use a set-header policy to drop or sanitize risky headers before cache lookup:
<policies>
<inbound>
<set-header name="X-Forwarded-Host" exists-action="delete" />
<set-header name="X-Host" exists-action="delete" />
<cache-lookup vary-by-developer="false" vary-by-deployment="false" vary-by-header="Accept,Accept-Encoding" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
</policies>
The vary-by-header attribute lists only the safe headers that should affect the cache.
Azure App Service (web.config) If you rely on IIS OutputCache, explicitly define the headers to vary by:
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ApiProfile" duration="60" varyByHeader="Accept,Accept-Encoding" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
By omitting Host and custom headers from varyByHeader, you prevent those values from altering the cache key.
Application‑level cache (Azure Redis Cache) When building a cache key in code, derive it solely from the route and query string:
using Microsoft.Extensions.Caching.Distributed;
using System.Text;
public string GetCacheKey(HttpRequest request)
{
var path = request.Path.Value.TrimEnd('/');
var query = request.QueryString.Value;
// Normalize query string order (optional but recommended)
var sortedQuery = string.IsNullOrEmpty(query) ? query :
string.Join("&", query.Split('&').OrderBy(s => s));
return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes($"{path}?{sortedQuery}"));
}
// Usage
var key = GetCacheKey(HttpContext.Request);
var cached = await _distributedCache.GetStringAsync(key);
This approach guarantees that attacker‑supplied headers never participate in the key, eliminating the poisoning vector.