From f4388a5cc33c85e7dc394b5814629f6427688232 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 21:23:15 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20stealth-route=20/cms/*=20=E2=80=94?= =?UTF-8?q?=20return=20404=20to=20unauthorized=20callers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any /cms/* hit (including exact /cms) that fails authorization returns 404 instead of redirecting to /account/login. CMS-PLAN §3.4 constraint. --- .../Middleware/CmsStealthRoutingHandler.cs | 34 +++++++++++++++++++ DeepDrftWeb/Program.cs | 7 ++++ 2 files changed, 41 insertions(+) create mode 100644 DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs diff --git a/DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs b/DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs new file mode 100644 index 0000000..8e88204 --- /dev/null +++ b/DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; + +namespace DeepDrftWeb.Middleware; + +/// +/// Returns 404 for any /cms/* request that fails authorization. +/// This prevents the CMS from acknowledging its own existence to unauthorized callers +/// (a redirect to /account/login would reveal that the route exists). +/// CMS-PLAN §3.4 stealth-routing constraint. +/// +public class CmsStealthRoutingHandler : IAuthorizationMiddlewareResultHandler +{ + private readonly AuthorizationMiddlewareResultHandler _default = new(); + + public async Task HandleAsync( + RequestDelegate next, + HttpContext context, + AuthorizationPolicy policy, + PolicyAuthorizationResult authorizeResult) + { + // For /cms/* routes (including an exact /cms hit), map any authorization + // failure to 404 regardless of cause (unauthenticated, wrong role, or any + // future policy failure). This prevents the CMS from acknowledging its + // own existence to callers outside the Admin hierarchy. + if (context.Request.Path.StartsWithSegments("/cms") && !authorizeResult.Succeeded) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + await _default.HandleAsync(next, context, policy, authorizeResult); + } +} diff --git a/DeepDrftWeb/Program.cs b/DeepDrftWeb/Program.cs index 01d0ff1..a4f5ad0 100644 --- a/DeepDrftWeb/Program.cs +++ b/DeepDrftWeb/Program.cs @@ -2,6 +2,8 @@ using AuthBlocksLib; using AuthBlocksLib.Options; using DeepDrftCms; using DeepDrftWeb; +using DeepDrftWeb.Middleware; +using Microsoft.AspNetCore.Authorization; using MudBlazor.Services; using DeepDrftWeb.Components; using Microsoft.AspNetCore.HttpOverrides; @@ -64,6 +66,11 @@ builder.Services.AddAuthBlocks(options => }; }); +// CMS stealth routing: unauthorized /cms/* requests return 404, not a redirect. +// This prevents the CMS from revealing its own existence to unauthenticated callers. +// See CMS-PLAN §3.4. +builder.Services.AddSingleton(); + // AuthBlocksWeb: Blazor JWT client services (auth API is mounted on this same host via MapAuthBlocks). // AuthBlocksWeb.Startup.ConfigureAuthServices registers AddCascadingAuthenticationState server-side. AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl);