using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Middleware; namespace AutopilotMonitor.Functions.Middleware; /// /// Stamps Cache-Control: no-store + Pragma: no-cache on responses for /// credential- or identity-bearing endpoints, preventing shared caches, browsers, and /// HTTP/1.0 proxies from retaining bearer tokens, SAS URLs, or PII session payloads. /// /// Allowlist over deny-all: a single centralized place to audit which endpoints are /// considered sensitive. New credential-returning endpoints must be added here. /// /// public class NoStoreCacheMiddleware : IFunctionsWorkerMiddleware { private const string CacheControlHeader = "Cache-Control"; private const string CacheControlValue = "no-store"; private const string PragmaHeader = "Pragma"; private const string PragmaValue = "no-cache"; /// /// Exact-match credential / identity endpoints. Bearer-token returning routes /// (bootstrap/validate, agent/upload-url) carry acute risk if cached. Session- /// list and raw-table surfaces are listed here for the literal path; their /// sub-routes (with {id}) are covered by the prefix table below. /// private static readonly string[] _exactRoutes = { "/api/auth/me", "/api/agent/config", "/api/agent/upload-url", // returns long-lived diagnostics SAS URL "/api/bootstrap/config", // Session list / search / diagnostic-download surfaces — PII session payloads "/api/sessions", "/api/diagnostics/download-url", // proxies diagnostics ZIP content // GA cross-tenant raw + global PII surfaces "/api/raw/sessions", "/api/raw/events", "/api/raw/events/search", "/api/global/sessions", "/api/global/distress-reports", "/api/global/session-reports", // covers list; /download-url + /{id}/note via prefix }; /// /// Prefix-match routes. Used for routes with path parameters where exact match /// is impossible. Anything beneath these prefixes is treated as sensitive. /// private static readonly string[] _prefixRoutes = { "/api/bootstrap/validate/", // /api/bootstrap/validate/{code} — Bearer token! "/api/config/", // /api/config/{tenantId} + sub-routes "/api/sessions/", // /api/sessions/{id} + sub-routes (events, signals, …) "/api/search/", // /api/search/{quick,sessions,sessions-by-event,sessions-by-cve} "/api/global/raw/", // GA cross-tenant raw event/session/table surfaces "/api/global/search/", // GA cross-tenant search results "/api/global/session-reports/", // /download-url (SAS) + /{reportId}/note }; public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next) { var httpContext = context.GetHttpContext(); if (httpContext != null) { var path = httpContext.Request.Path.Value ?? string.Empty; if (IsSensitive(path)) { // Direct-write before next() — OnStarting() is not reliably triggered // in the .NET 8 isolated worker (the host bridges the worker's response, // so the hook fires on a shadow object that never reaches the wire). // Same pattern used by CorrelationIdMiddleware and TimingAllowOriginMiddleware. if (!httpContext.Response.Headers.ContainsKey(CacheControlHeader)) httpContext.Response.Headers[CacheControlHeader] = CacheControlValue; if (!httpContext.Response.Headers.ContainsKey(PragmaHeader)) httpContext.Response.Headers[PragmaHeader] = PragmaValue; } } await next(context); } internal static bool IsSensitive(string path) { if (string.IsNullOrEmpty(path)) return false; foreach (var exact in _exactRoutes) { if (path.Equals(exact, StringComparison.OrdinalIgnoreCase)) return true; } foreach (var prefix in _prefixRoutes) { if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return true; } return false; } }