Replay Attack in Aspnet with Mutual Tls
Replay Attack in Aspnet with Mutual Tls — how this specific combination creates or exposes the vulnerability
A replay attack in an ASP.NET API secured with mutual TLS (mTLS) occurs when an adversary captures a valid client certificate + encrypted request and retransmits it to the server to produce the same effect as the original interaction. Even with mTLS providing strong transport-layer authentication, the protocol itself does not prevent an attacker from replaying a legitimate TLS session. The server validates the client certificate and completes the TLS handshake, but it has no mechanism to detect duplication of an already-accepted request. This gap is common in APIs that treat mTLS as sufficient authentication without additional application-level protections.
In the ASP.NET stack, this risk is pronounced when endpoints are idempotent (e.g., GET or idempotent POST) or when state is not enforced at the application layer. For example, an order-creation endpoint that accepts a POST with a payment token and client certificate can be replayed to charge a user multiple times if the server does not track request uniqueness. The mTLS handshake authenticates the client to the server, but it does not bind the request content to a nonce, timestamp, or session context that survives across replays. MiddleBrick’s 12 security checks highlight this scenario under Authentication and BOLA/IDOR, noting that unauthenticated attack surface testing can surface missing replay protections despite valid certificates.
Real-world patterns that exacerbate the issue include missing nonce or timestamp headers, lack of idempotency keys, and permissive retry logic on the server. In an OpenAPI/Swagger spec analyzed with full $ref resolution, missing security scheme details for replay mitigation can lead to inconsistent runtime behavior. Attackers exploit such inconsistencies by replaying captured requests to test for missing rate limiting, weak input validation, or missing state checks. MiddleBrick’s active testing probes can surface these weaknesses by submitting repeated requests and observing whether the application distinguishes between first and subsequent submissions.
Mutual Tls-Specific Remediation in Aspnet — concrete code fixes
To mitigate replay attacks in ASP.NET when using mutual TLS, combine mTLS with application-level protections such as nonces, timestamps, and idempotency keys. Below are concrete code examples that you can integrate into your ASP.NET services.
1. Enforce Mutual TLS in ASP.NET
Configure Kestrel to require client certificates and validate them explicitly. This ensures that only trusted clients can establish a TLS session.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
httpsOptions.AllowedClientCertificates.Add(new X509Certificate2("path/to/ca.crt"));
// Optionally set revocation mode
httpsOptions.ClientCertificateRevocationCheck = CertificateRevocationMode.Online;
});
});
// Optional: inspect the client cert in middleware
app.Use((context, next) =>
{
var cert = context.Connection.ClientCertificate;
if (cert == null)
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync("Client certificate required.");
}
// Additional validation: thumbprint, subject, etc.
if (cert.Thumbprint != "EXPECTED_THUMBPRINT")
{
context.Response.StatusCode = 403;
return context.Response.WriteAsync("Forbidden client certificate.");
}
return next();
});
app.Run();
2. Add a Timestamp and Nonce Header
Require clients to include a timestamp and a nonce in headers. The server validates freshness (e.g., within 5 minutes) and uniqueness (e.g., one-time use per nonce).
// Request model with replay metadata
public class ReplayProtectedRequest
{
public string Nonce { get; set; } = string.Empty;
public long Timestamp { get; set; } // Unix seconds
// other payload fields...
}
// Middleware to validate replay protection
public class ReplayProtectionMiddleware
{
private readonly RequestDelegate _next;
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
private readonly TimeSpan _maxAge = TimeSpan.FromMinutes(5);
public ReplayProtectionMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Headers.TryGetValue("X-Nonce", out var nonceHeader) &
context.Request.Headers.TryGetValue("X-Timestamp", out var tsHeader) &&
long.TryParse(tsHeader, out var ts))
{
var requestTime = DateTimeOffset.FromUnixTimeSeconds(ts).UtcDateTime;
if (DateTime.UtcNow - requestTime > _maxAge)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Timestamp too old.");
return;
}
var cacheKey = $"nonce:{nonceHeader}";
if (_cache.TryGetValue(cacheKey, out _))
{
context.Response.StatusCode = 409;
await context.Response.WriteAsync("Nonce already used.");
return;
}
_cache.Set(cacheKey, true, DateTimeOffset.UtcNow + _maxAge);
}
else
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Missing replay protection headers.");
return;
}
await _next(context);
}
}
// In Program.cs
app.UseMiddleware<ReplayProtectionMiddleware>();
3. Use Idempotency Keys for State-Changing Operations
For POST/PUT/DELETE, require an idempotency key header. Store the key + response for a defined window so duplicates return the original result without re-executing side effects.
// Idempotency middleware example
public class IdempotencyMiddleware
{
private readonly RequestDelegate _next;
private static readonly ConcurrentDictionary<string, (DateTime Expiry, object Response)> _store = new();
private readonly TimeSpan _window = TimeSpan.FromHours(1);
public IdempotencyMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Method is "POST" or "PUT" or "DELETE")
{
if (!context.Request.Headers.TryGetValue("Idempotency-Key", out var key) || string.IsNullOrWhiteSpace(key))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Idempotency-Key header required.");
return;
}
if (_store.TryGetValue(key, out var entry) && DateTime.UtcNow < entry.Expiry)
{
// Return cached response for duplicate
var json = JsonSerializer.Serialize(entry.Response);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(json);
return;
}
// Capture response via a wrapper (simplified)
var originalBody = context.Response.Body;
using var ms = new MemoryStream();
context.Response.Body = ms;
await _next(context);
if (context.Response.StatusCode is 200 or 201)
{
ms.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(ms);
var responseBody = await reader.ReadToEndAsync();
ms.Seek(0, SeekOrigin.Begin);
await ms.CopyToAsync(originalBody);
_store[key] = (DateTime.UtcNow + _window, responseBody);
}
else
{
ms.Seek(0, SeekOrigin.Begin);
await ms.CopyToAsync(originalBody);
}
context.Response.Body = originalBody;
}
else
{
await _next(context);
}
}
}
// In Program.cs
app.UseMiddleware<IdempotencyMiddleware>();
These patterns align with best practices for secure API design and map to checks in MiddleBrick’s scans under Authentication, BOLA/IDOR, and Property Authorization. They do not introduce automatic fixes but provide clear remediation guidance to help developers implement replay resistance while continuing to rely on mTLS for transport authentication.