Skip to content

Security hardening, duplicated-logic cleanup, and doc updates - #5

Merged
TenOfNine merged 1 commit into
mainfrom
security/hardening-and-docs
Aug 10, 2026
Merged

Security hardening, duplicated-logic cleanup, and doc updates#5
TenOfNine merged 1 commit into
mainfrom
security/hardening-and-docs

Conversation

@TenOfNine

Copy link
Copy Markdown
Owner

Summary

A full security review of the repository, a pass to consolidate duplicated logic, source comments on the non-obvious parts, and updated documentation (including a new deployment flow diagram).

Security

  • Closes the biggest gap in the app: it previously had no authentication layer at all — anyone who could reach the deployed URL had full read/write access to the dashboard and setup wizard, including the ability to rotate the Graph client secret and add/remove monitored mailboxes. This was already flagged as the top accepted gap (NFR-SEC-5) in the technical spec. It's now closed by gating the whole app behind Microsoft Entra ID sign-in via Azure App Service Authentication ("Easy Auth" v2), enforced at the platform level — no application code involved, on by default (enableEntraIdAuth = true in infra/main.bicep).
  • New SecurityHeadersMiddleware: a strict, per-request-nonce Content-Security-Policy (default-src 'self', nothing loaded from a CDN — everything under wwwroot/lib is vendored), X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and the Kestrel Server header suppressed.
  • XXE / entity-expansion hardening: the DMARC XML parser now explicitly prohibits DTD processing and nulls the XmlResolver before parsing, rather than relying on .NET's implicit (already-safe) defaults — reporting organizations are untrusted third parties on the internet.
  • Decompression-bomb guard: zip/gzip attachment extraction is capped at 50 MB of actual bytes copied (not the archive's declared, attacker-controlled size) — the shared mailbox accepts attachments from arbitrary senders, so this is real attack surface.
  • New tests for all three guards (DOCTYPE rejection, zip/gzip decompression limits).

Duplicated-logic cleanup

Found via a dedicated review pass:

  • DmarcAlignment.IsAlignedPass — single source of truth for the "aligned pass" rule, previously reimplemented separately in the ingestion pipeline, the chart API, and the dashboard summary.
  • The dashboard's "verified" badge now references the existing SenderLegitimacyEvaluator.VerifiedAlignedPassRatioThreshold constant instead of a disconnected magic 95.
  • DbContext.GetOrCreateSingletonAsync removes the repeated "FindAsync → null check → new → conditionally Add" ceremony duplicated across the Setup wizard's singleton-settings pages.

Docs

  • Technical specification bumped to v1.4: NFR-SEC-5 rewritten (Easy Auth), new NFR-SEC-7–9 (headers/XXE/decompression-bomb), updated test counts and verification evidence.
  • docs/deployment.md reworked with a Mermaid flow diagram covering the full deployment journey (Entra ID app registrations → GitHub config → deploy workflow → first-run setup wizard) and a step-by-step Easy Auth setup section — the three app registrations involved are easy to conflate, so the diagram and an explicit table spell out which is which.
  • README: new Security section, updated test-count references.

Test plan

  • dotnet build -c Release — 0 warnings, 0 errors
  • dotnet format --verify-no-changes — clean
  • dotnet test — 65/65 passing (62 previous + 3 new security-guard tests)
  • Both Bicep templates compile with the Bicep CLI (v0.46.1) — 0 errors, 0 warnings (including the new authsettingsV2 resource)
  • Verified against a running instance (headless browser): security headers present on every route; the domain-detail trend chart (the app's one inline <script>) still renders correctly under the new nonce-based CSP with zero console errors
  • Live Azure deploy exercising Easy Auth end-to-end (no Azure access in this environment — same documented limitation as the rest of infra/)

Generated by Claude Code

Security:
- Gate the whole app (setup wizard included) behind Microsoft Entra ID
  sign-in via Azure App Service Authentication ("Easy Auth" v2), enforced
  at the platform level. Previously the app had no authentication layer at
  all -- anyone who could reach the URL had full read/write access,
  including the ability to rotate the Graph client secret. This was
  already flagged as the top accepted gap (NFR-SEC-5) in the technical
  spec; it's now closed by default (enableEntraIdAuth = true).
- Add SecurityHeadersMiddleware: a strict per-request-nonce Content-
  Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-
  Policy, Permissions-Policy, and suppress Kestrel's Server header.
- Harden the DMARC XML parser against XXE/entity-expansion by explicitly
  prohibiting DTD processing and nulling the XmlResolver, rather than
  relying on .NET's implicit (already-safe) defaults.
- Cap zip/gzip attachment decompression at 50 MB of actual bytes copied
  (not the archive's declared, attacker-controlled size) -- the shared
  mailbox accepts attachments from arbitrary internet senders, so a
  decompression bomb is real attack surface, not theoretical.
- Add tests for all three new guards (DOCTYPE rejection, zip/gzip
  decompression limits).

Duplicated-logic cleanup (found via a dedicated review pass):
- Add DmarcAlignment.IsAlignedPass as the single source of truth for the
  "aligned pass" rule, previously reimplemented separately in the
  ingestion pipeline, the chart API, and the dashboard summary.
- Reference the existing SenderLegitimacyEvaluator.VerifiedAlignedPassRatioThreshold
  constant from the dashboard's badge instead of a disconnected magic 95.
- Add DbContext.GetOrCreateSingletonAsync to remove the repeated
  "FindAsync -> null check -> new -> conditionally Add" ceremony
  duplicated across the Setup wizard's singleton-settings pages.

Docs:
- Bump the technical specification to v1.4: new NFR-SEC-5 (Easy Auth),
  NFR-SEC-7-9 (headers/XXE/decompression-bomb), updated test counts and
  verification evidence (including a headless-browser CSP/nonce check).
- Rework docs/deployment.md with a Mermaid flow diagram covering the full
  deployment journey (Entra ID app registrations -> GitHub config ->
  deploy workflow -> first-run setup wizard) and a step-by-step Easy Auth
  setup section.
- Add a README Security section and update test-count references.

All 65 tests pass; dotnet format --verify-no-changes is clean; both
Bicep templates compile with 0 errors/0 warnings (validated with the
Bicep CLI); the CSP/nonce change was verified against a running instance
via a headless-browser pass with zero console errors.
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@TenOfNine
TenOfNine merged commit 137b6b1 into main Aug 10, 2026
4 checks passed
@TenOfNine
TenOfNine deleted the security/hardening-and-docs branch August 10, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants