Header Injection in Aspnet with Hmac Signatures
Header Injection in Aspnet with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Header Injection in ASP.NET occurs when untrusted input is reflected into HTTP headers without validation or encoding. When HMAC signatures are used to authenticate requests, an attacker may attempt to inject additional headers (e.g., X-Forwarded-For, Authorization, or custom headers) to bypass signature validation or influence server behavior. Because HMAC signatures are typically computed over a subset of headers and the request payload, injected headers that are not included in the signature can change server logic without invalidating the signature.
In ASP.NET, this often manifests when developer code builds headers dynamically (for example, forwarding or logging headers) and concatenates user-controlled values without strict allowlists. If the server uses a header value in routing, content negotiation, or security decisions, the injected header can trigger SSRF, cache poisoning, or security logic bypass. Even when HMAC validates the request integrity, the server may still process malicious injected headers before signature checks are applied, depending on the order of middleware and the scope of what is signed.
Consider a scenario where an API expects an X-Request-ID header and signs a subset of headers with HMAC. An attacker sends:
X-Request-ID: abc123
X-Forwarded-For: 127.0.0.1
X-Signature: ...
If the server reads X-Forwarded-For to determine the client IP and uses that in business logic (e.g., rate limiting or tenant resolution) without including it in the HMAC, the attacker can influence internal behavior while the signature remains valid. This combination therefore exposes the application to logic bypasses and request manipulation despite the presence of HMAC signatures.
Hmac Signatures-Specific Remediation in Aspnet — concrete code fixes
To mitigate Header Injection when using HMAC signatures in ASP.NET, ensure strict header allowlisting, canonicalize the signed string exactly as verified, and avoid using potentially mutable or injected headers for server-side decisions. The following practices and code examples focus on .NET 6+ minimal APIs and common HMAC patterns.
1) Canonicalize signed headers and reject unexpected headers
Define an allowlist of headers that can be signed and are safe to use. Reject requests that contain unexpected headers, or at least do not use them for routing or security decisions.
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string[] allowedSignedHeaders = [ "x-request-id", "content-type" ];
string sharedSecret = builder.Configuration["HmacSecret"] ?? string.Empty;
app.Use(async (context, next)
{
// Reject injected or unexpected headers before processing
foreach (var header in context.Request.Headers)
{
if (!allowedSignedHeaders.Contains(header.Key.ToLowerInvariant()))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync($"Unexpected header: {header.Key}");
return;
}
}
await next();
});
app.MapPost("/webhook", (HttpRequest req) =>
{
// Build canonical string exactly as the sender did
var sb = new StringBuilder();
foreach (var h in allowedSignedHeaders)
{
if (req.Headers.TryGetValue(h, out var val))
sb.Append($"{h.ToLowerInvariant()}:{val}");
else
sb.Append($"{h.ToLowerInvariant()}:");
}
sb.Append(req.Body.Length > 0 ? new StreamReader(req.Body).ReadToEnd() : string.Empty);
string payload = sb.ToString();
string receivedSignature = req.Headers["X-Signature"];
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(sharedSecret));
string computedSignature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)));
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(computedSignature), Encoding.UTF8.GetBytes(receivedSignature)))
throw new InvalidOperationException("Invalid signature");
// Safe to proceed: headers are allowlisted and signature matches canonical form
return Results.Ok();
});
app.Run();
2) Avoid using injected headers in HMAC canonicalization
Ensure the canonical string used to compute and verify HMAC does not include attacker-controlled headers unless they are also included in the allowlist and verified before use. Never include headers like X-Forwarded-For or X-Original-URL in the signed string unless they are explicitly expected and validated.
// BAD: includes X-Forwarded-For which can be injected
var badPayload = $"x-forwarded-for:{req.Headers["X-Forwarded-For"]}x-request-id:{req.Headers["X-Request-ID"]}";
// GOOD: only allowlisted headers are used
var goodPayload = $"x-request-id:{req.Headers["X-Request-ID"]}content-type:{req.Headers["Content-Type"]}";
3) Validate and sanitize header values used in server logic
If you must use a header value in server logic (e.g., tenant ID), validate format and length, and avoid using it directly in paths or queries to prevent SSRF or injection. Also ensure the header is not duplicated by proxies that could inject additional values.
string tenantId = req.Headers["X-Tenant-ID"];
if (!System.Text.RegularExpressions.Regex.IsMatch(tenantId, @"^[a-zA-Z0-9\-]{1,64}$"))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Invalid tenant ID");
return;
}
// Use tenantId safely after validation