Files
slp-modular-cms/aidlc-docs/features/gitea-deployment-workflow/construction/u3-security-headers/code/generation-summary.md
T
SluijsensandClaude Opus 5 a122548454 Sends the security headers from the application instead of the proxy
U3. These headers normally come from nginx, but the deployment target does
not allow server configuration, so the application emits them itself. That
changes the failure mode: a bad nginx config fails loudly at reload, while a
middleware that never runs sends nothing and says nothing.

Two policies, defined in code. Strict for /admin, /api/v1 and /health;
relaxed for the public website, which is authored elsewhere by someone who
has never seen this policy. Configuration decides where a policy applies and
which external origins are permitted; it cannot invent a policy that is
subtly permissive.

script-src 'self' under Strict has no 'unsafe-inline' and no 'unsafe-eval',
asserted by a test so that loosening it means deleting a test that says why.
style-src does carry 'unsafe-inline' and cannot not: Radix positions its
overlays with inline style attributes, which nonces cannot reach at all.

Two traps handled explicitly. StartsWithSegments rather than string
StartsWith, because "/administrator".StartsWith("/admin") is true and a
public page would silently lose its inline scripts with no server-side trace.
And all decision logic sits in a static writer rather than in the middleware,
because DefaultHttpContext.Response.OnStarting is a no-op — the obvious
middleware test observes nothing and an assertion that nothing was written
passes for entirely the wrong reason.

An unknown policy name fails ValidateOnStart, so the process exits rather
than quietly serving /admin under the relaxed policy. Origin format is
validated too, beyond what the design asked: a CSP source list silently
ignores a malformed source, so a URL with a path would look configured and
block the script anyway.

BR-U3-22's Umami startup warning is withdrawn (REF-U3-01) — the backend
never sees VITE_UMAMI_WEBSITE_ID. It becomes a blocking CI gate in U5.

Build 0 errors; 315 tests pass, up from 253.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 10:58:45 +02:00

6.2 KiB

Code Generation Summary — U3 HTTP Security Headers & CSP

Generated: 2026-07-28 Verified: dotnet build SlpModularCms.sln -c Release0 errors; dotnet test315 passed, 0 failed (baseline 253, so +62)


Files Created

File Purpose
Core/Hosting/Security/SecurityHeadersOptions.cs SecurityHeadersOptions + PathPolicyRule. Defaults are the production values
Core/Hosting/Security/CspPolicyCatalog.cs SecurityHeaderSet record + the two policy definitions as a pure function
Core/Hosting/Security/CspPolicyProvider.cs ICspPolicyProvider + FrozenDictionary-backed implementation
Core/Hosting/Security/PathPolicyResolver.cs Ordered, segment-aware prefix matching
Core/Hosting/Security/SecurityHeaderWriter.cs All decision logic, static and dependency-free
Core/Hosting/Security/SecurityHeadersMiddleware.cs Glue: gate, resolve, register the response-start callback
Core/Hosting/Security/SecurityHeadersExtensions.cs AddCmsSecurityHeaders / UseCmsSecurityHeaders + startup logging

Test Files Created

File Tests Covers
Core.Tests/Hosting/Security/SecurityHeaderWriterTests.cs 20 Per-header scoping, HSTS gating, never-overwrite, content-type parsing
Core.Tests/Hosting/Security/CspPolicyCatalogTests.cs 24 Directive content, script-src 'self' under Strict, origin injection, unknown policy
Core.Tests/Hosting/Security/PathPolicyResolverTests.cs 15 Segment matching, /administrator vs /admin, first-match-wins, normalisation
Core.Tests/Hosting/Security/SecurityHeadersOptionsValidationTests.cs 15 ValidateOnStart through a real container

Files Modified

File Change
Api/Program.cs AddCmsSecurityHeaders; UseCmsSecurityHeaders first inside UseExceptionHandler
Api.Slave/Program.cs Same. The slave serves /api/v1 and /health and is reached during diagnosis
Api/appsettings.json SecurityHeaders section — three strict prefixes
Api.Slave/appsettings.json SecurityHeaders section — two strict prefixes (no /admin there)

Business Rule Coverage

Rule Where Test
BR-U3-01 nosniff on every response SecurityHeaderWriter.Apply Apply_ShouldWriteNosniff_ForAScriptFile
BR-U3-02 HSTS except in Development sendHsts parameter Apply_ShouldSkipHsts_WhenSendHstsIsFalse
BR-U3-03 CSP/Frame/Referrer on HTML only IsHtml early return Apply_ShouldWriteOnlyAlwaysHeaders_ForNonHtmlResponses
BR-U3-04 never overwrite TryAdd Apply_ShouldNotOverwrite_HeadersAlreadyPresent
BR-U3-05 write at response start Response.OnStarting Carried to Build and Test
BR-U3-06 before static files Program.cs position Carried to Build and Test
BR-U3-07 never throw per request catch-and-log in the callback Carried to Build and Test
BR-U3-08 all but HSTS in Development sendHsts: false path Apply_ShouldSkipHsts_WhenSendHstsIsFalse
BR-U3-09 headers on error responses inside UseExceptionHandler Carried to Build and Test
BR-U3-10…12 two policies, config mapping CspPolicyCatalog, PathPolicyResolver Catalog + resolver suites
BR-U3-13 script-src 'self' under Strict CspPolicyCatalog.Build Strict_ShouldNotAllowInlineOrEvalScript
BR-U3-14 style-src 'unsafe-inline' CspPolicyCatalog.Build Strict_ShouldAllowInlineStyle_ButOnlyStyle
BR-U3-15, BR-U3-16 relaxed is enforcing, no external script CspPolicyCatalog.Build Relaxed_ShouldNotPermitExternalScriptOrigins_ByDefault
BR-U3-17 object-src/base-uri CspPolicyCatalog.Build BothPolicies_ShouldBlockPluginsAndBaseTagInjection
BR-U3-18 composed once FrozenDictionary in the constructor Provider_ShouldComposeBothPolicies_WithNoOriginsConfigured
BR-U3-19 frame options per policy CspPolicyCatalog.Build Strict_ShouldDenyFramingEntirely, Relaxed_ShouldAllowSameOriginFraming_…
BR-U3-20 unknown name fatal ValidateOnStart Validation_ShouldFail_ForAnUnknownPolicyNameInPathPolicies
BR-U3-21 empty origin lists valid Join returns the base list Validation_ShouldPass_WithNoConfigurationAtAll
BR-U3-22 Umami origin warning Withdrawn — REF-U3-01, moved to the U5 CI gate n/a
BR-U3-23 log permitted origins UseCmsSecurityHeaders Carried to Build and Test
BR-U3-24 disable logs a warning UseCmsSecurityHeaders Carried to Build and Test

Deviations and Additions

One rule withdrawn, one check added. REF-U3-01 (raised at NFR Design): BR-U3-22's Umami-origin startup warning is not implementable, because the backend never sees VITE_UMAMI_WEBSITE_ID. Replaced by a blocking U5 CI gate. Nothing in the generated code attempts it.

Origin format validation added beyond the functional design. AllowedScriptOrigins and AllowedConnectOrigins are validated at startup to be scheme-and-host without a path. A CSP source list silently ignores a malformed source, so https://analytics.example.com/script.js in configuration would produce a policy that looks configured and blocks the script anyway. Caught at startup instead.


Carried to Phase-Level Build and Test

Everything below needs a real response feature or a running host — DefaultHttpContext.Response.OnStarting is a no-op, so none of it can be asserted in a unit test without asserting the wrong thing:

Behaviour Why it matters
Headers present on a static website asset Verifies the registration sits before UseCmsStaticContent. A misordered registration compiles, starts, passes all 315 tests and serves the entire website with no CSP
Headers present on the availability gate's 503 and on an unhandled-exception ProblemDetails Verifies the position inside UseExceptionHandler (BR-U3-09)
/admin/dashboard gets DENY, / gets SAMEORIGIN End-to-end policy selection
/admin/assets/*.js gets only nosniff + HSTS Content-type scoping against real static-file responses
Startup log lines for permitted origins and for Enabled: false BR-U3-23, BR-U3-24
Both hosts start with the new section present ValidateOnStart against the committed appsettings.json