---
key: security.baseline-response-headers
title: '2026-07-11 — Baseline security response headers + Phase-0 API hardening (#197, PR #279)'
status: active
since: '2026-07-11'
supersedes: none
superseded-by: none
rule: '`SecurityHeadersMiddleware`, registered first in the pipeline, sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, and `Referrer-Policy: strict-origin-when-cross-origin` on every response (CSP/HSTS deliberately deferred); API-key comparison is constant-time and playout pagination is clamped.'
signals: 'security headers, nosniff, Phase-0 hardening, constant-time comparison · paths: `ErsatzTV/Middleware/SecurityHeadersMiddleware` · issues: #197, #279, #283'
mechanics: '`ErsatzTV/Middleware/SecurityHeadersMiddleware`'
---

Phase-0 of the #197 remediation — the posture-**independent** safe subset, shipped ahead of the
fail-closed/CORS/versioning posture work tracked in #280–#289.

- **Baseline security headers on every response.** New `ErsatzTV/Middleware/SecurityHeadersMiddleware`,
  registered **first** in the pipeline (before the `/iptv` `MapWhen` branch and `UseCors`), so it covers
  `/api`, `/iptv`, `/artwork`, static, the SPA fallback, and filter-produced 4xx alike — which is why it's
  middleware, not an MVC filter. It sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, and
  `Referrer-Policy: strict-origin-when-cross-origin`. `nosniff` is the standing backstop for the artwork
  content-type MIME-sniffing risk (#283). **CSP and HSTS are deliberately NOT included here**: CSP must be
  validated against the ChicoryTV SPA's inline assets, and HSTS is a proxy/TLS-termination decision — both
  belong to the #197 posture design (#284/roadmap), not this baseline. Headers are set eagerly (not via
  `Response.OnStarting`); safe today because the pipeline has no `UseExceptionHandler`/`UseStatusCodePages`
  that would `Response.Clear()` — switch to `OnStarting` if one is ever added.
- **Constant-time API-key comparison.** `ApiKeyAuthorizationFilter` compares `X-Api-Key` with
  `CryptographicOperations.FixedTimeEquals` (over UTF-8 bytes) instead of ordinal `string.Equals`, removing
  the response-timing oracle on the write key. Accept/reject behavior is otherwise identical.
- **Playout pagination clamped.** `GET /api/playouts` and `GET /api/playouts/{id}/items` now clamp
  `Math.Clamp(pageSize, 1, 100)` + `Math.Max(0, pageNum)` before the query — applying the api-conventions §1
  clamp convention the other paged endpoints already follow (these two were passing the raw client value
  straight to EF `Take()`).

The larger #197 posture (fail-closed writes, sensitive-read auth tier, CORS lockdown, `/api/v1` versioning,
the OpenAPI security scheme) is decomposed into #280–#289 with the phased roadmap on #197; those PRs will
append their own decisions here as they land.
