Cache Poisoning in Aspnet with Hmac Signatures
Cache Poisoning in Aspnet with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Cache poisoning in ASP.NET occurs when an attacker causes a cache (for example output cache, response cache, or a downstream proxy cache) to store and later serve attacker-controlled content to other users. Even when responses are protected with HMAC signatures for integrity, misconfiguration or implementation gaps can allow poisoned cache entries to bypass validation or be treated as valid for different users or contexts.
HMAC signatures in ASP.NET are commonly used to ensure that cached entries or anti-forgery tokens haven’t been tampered with. A typical pattern involves signing a payload (such as a cache key fragment or a serialized view model) with a server-side secret and attaching the signature to the response or to a hidden field. The server later recomputes the HMAC and compares it to the received signature before using the cached data. This mechanism is effective only if the signed data uniquely and reliably identifies the user, the request context, and the cache scope.
When HMAC signatures are used but the signed payload does not include critical contextual elements—such as the authenticated user ID, tenant or tenant-specific salt, request path, query parameters that affect output, or the current cache region—two problems arise. First, a cached entry signed for one user or one query variant may be reused for another user or variant, enabling user-to-user cache poisoning. Second, an attacker who can partially control or predict parts of the signed input (for example, a nonced prefix combined with a mutable query parameter) may manipulate the cache key construction outside the signed portion, leading to substitution attacks where the cache returns attacker-controlled content while the signature still validates.
ASP.NET’s output cache and data caching APIs do not automatically bind the cache key to the full request context when you use custom HMAC logic. If developers build cache keys manually (e.g., concatenating query strings and signing them) but omit the user identity or a per-request nonce, the same signed key may be shared across users. An authenticated attacker who can craft requests that result in similar or colliding cache keys can cause the cache to store a response that includes sensitive data or malicious content for other users to receive. Real-world attack patterns such as HTTP Parameter Pollution or host-header manipulation can exacerbate this when cache keys are derived from mutable headers or query parameters that are not part of the HMAC.
Additionally, if the HMAC key management is weak (for example, using a static key across deployments or exposing key rotation practices), or if the signature is attached in an unsigned parameter that an attacker can modify, the integrity guarantee is undermined. In extreme cases, an attacker might leverage insecure deserialization combined with predictable cache keys to inject malicious objects into the cache, which are later served and executed in the context of trusting clients. Therefore, when relying on HMAC signatures in ASP.NET, it is essential to include all context that influences the cached output in the signed payload and to treat the cache as hostile, validating both the signature and the scope of the cached content before use.
Hmac Signatures-Specific Remediation in Aspnet — concrete code fixes
To remediate cache poisoning risks when using HMAC signatures in ASP.NET, bind the signature to a comprehensive representation of the request context, including user identity, tenant or isolation scope, path, query parameters that affect output, and any cache-segmenting metadata. Use a canonical serialization method to build the signed payload, and validate the scope before using cached data. Below are concrete code examples that demonstrate a secure approach.
Example 1: HMAC-signed cache key with user and query context in ASP.NET Core
using System;using System.Security.Cryptography;using System.Text;using System.Text.Encodings.Web;using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Caching.Distributed;
public class SignedCacheService{ private readonly byte[] _key; private readonly IDistributedCache _cache; private readonly IHttpContextAccessor _httpContextAccessor;
public SignedCacheService(byte[] key, IDistributedCache cache, IHttpContextAccessor httpContextAccessor) { _key = key; _cache = cache; _httpContextAccessor = httpContextAccessor; }
public string GetOrCreate(string baseKey, Func<string> factory, TimeSpan expiration) { var context = _httpContextAccessor.HttpContext; if (context is null) throw new InvalidOperationException("No HTTP context available.");
// Include elements that uniquely identify the request and user var userId = context.User.Identity?.IsAuthenticated == true ? context.User.FindFirst("sub")?.Value : "anonymous"; var path = context.Request.Path.Value ?? string.Empty; var query = context.Request.QueryString.Value ?? string.Empty;
// Canonicalize query parameters (sort, exclude signature parameters) var queryParams = QueryHelpers.ParseQuery(context.Request.QueryString.Value.Substring(1)); var sortedKeys = new System.Collections.Generic.SortedSet(queryParams.Keys); var canonicalQuery = string.Join("&", sortedKeys.Select(k => $"{k}={string.Join(",", queryParams[k])}"));
var payload = $"{userId}|{path}|{canonicalQuery}|{baseKey}"; var signature = ComputeHmac(payload, _key); var cacheKey = $"{baseKey}:sig={signature}";
var cached = _cache.GetString(cacheKey); if (cached != null) return cached;
cached = factory(); _cache.SetString(cacheKey, cached, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiration }); return cached; }
private static string ComputeHmac(string data, byte[] key) { using var hmac = new HMACSHA256(key); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data)); return Convert.ToBase64String(hash); }}
Example 2: Validating scope before using cached output in an MVC action
using Microsoft.AspNetCore.Mvc;using System.Security.Cryptography;using System.Text;using System.Text.Encodings.Web;
public class ProfileController : Controller{ private readonly byte[] _key;
public ProfileController() { /* load key securely, e.g., from configuration with rotation support */ _key = Convert.FromBase64String(Configuration["CacheHmacKey"]); }
[HttpGet("/profile")]
public IActionResult Profile(string section) { var userId = User.FindFirst("sub")?.Value ?? "anon"; var path = "/profile"; var canonicalSection = string.IsNullOrWhiteSpace(section) ? "default" : section; var payload = $"{userId}{path}{canonicalSection}"; var expectedSig = ComputeHmac(payload, _key);
if (!string.Equals(expectedSig, Request.Query["sig"], StringComparison.Ordinal)) { return BadRequest("Invalid signature or scope mismatch."); }
// Build cache key that includes signature for distributed cache compatibility var cacheKey = $"profile:{userId}:{canonicalSection}:sig={expectedSig}"; var cached = MemoryCache.Default.Get(cacheKey) as string;
if (cached != null) { return Content(cached, "text/html"); }
// Simulate expensive operation var content = $"<div>Profile section: {canonicalSection} for user {userId}</div>"; MemoryCache.Default.Set(cacheKey, content, DateTimeOffset.UtcNow.AddMinutes(5));
return Content(content, "text/html"); }
private static string ComputeHmac(string data, byte[] key) { using var hmac = new HMACSHA256(key); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data)); return Convert.ToBase64String(hash); }}
Best practices summary
- Include user ID, tenant/realm, request path, normalized query string, and any cache-affecting headers in the HMAC payload.
- Use a canonical serialization for query parameters (sorted, consistently encoded) to avoid equivalent but differing keys.
- Treat the cache as hostile: always recompute scope and verify the HMAC before using cached content, and do not rely on signature alone to authorize data visibility.
- Rotate keys periodically and avoid sharing static keys across services; consider key identifiers (kid) in signatures if multiple keys are used.
- Combine HMAC checks with ASP.NET’s built-in anti-forgery and authorization mechanisms to ensure that cache scope aligns with authentication and consent boundaries.