Skip to content

Security: shalant/PortfolioNov25

Security

docs/SECURITY.md

Security Policy & Audit

Date: 2026-08-10 Status: Reasonably secure for what this actually is. No critical issues found.


Overview

This is a personal portfolio site with a static blog. Security is straightforward because there's no user authentication, database, or server-side logic on the live site.

Actual infrastructure (verified 2026-08-10):

  • Hosting: GitHub Pages — static files only, no server, no Azure
  • Deploy: GitHub Actions (.github/workflows/publish-gh-pages.yml) builds the Blazor WASM app and publishes to Pages on every push to main
  • DNS: domain registered elsewhere, DNS records managed via a separate DNS panel, pointed at GitHub Pages via a CNAME file in wwwroot/
  • BackendTools: a separate Blazor Server app, run locally only (dotnet run), never deployed anywhere

An earlier version of this document assumed Azure App Service hosting (Azure Portal custom headers, Web.config, Azure Key Vault). None of that applies. If you read older recommendations referencing Azure elsewhere in this repo's history, they don't reflect reality — GitHub Pages has no server to configure.

Security Audit Summary ✅

What's Secure

  • ✅ HTTPS/TLS — GitHub Pages enforces HTTPS on the custom domain automatically
  • ✅ No secrets in code — verified via repo-wide scan for API key patterns, private key blocks; none found
  • ✅ No .env or per-environment appsettings.*.json tracked — and now actively gitignored (see below)
  • ✅ XSS prevention — no inline event handlers; the one MarkupString usage (blog post HTML content) renders content generated by the Claude API from your own input, not arbitrary user input
  • ✅ Blazor WebAssembly — client-only, nothing to exploit server-side because there is no server
  • ✅ File upload safety (BackendTools, local-only) — images capped at 5MB, format-validated
  • ✅ No .user/build-artifact files accidentally tracked in git

What's Genuinely Different From What You Might Expect

  • 🟡 "Contact form" is a mailto: link, not a real form. Contact.razor and ConsultingPage.razor both link to mailto:doug.rosenberg@gmail.com — no submission is ever sent to or stored by this app. That means: no server-side validation/rate-limiting/CAPTCHA is needed because there's no submission endpoint to abuse, but it also means you never capture a lead's info — visitors have to actually send the email themselves. If you want submissions captured, that's a feature decision (e.g. a form service like Formspree, or a small Azure Function), not a security fix.
  • 🔴 Security headers (CSP, X-Frame-Options, HSTS, etc.) are NOT achievable via GitHub Pages configuration. GitHub Pages serves static files with a fixed set of response headers you cannot customize — no Web.config equivalent, no _headers file support (that's a Netlify/Cloudflare Pages feature), no portal setting. The only way to add these headers to dougrosenbergdev.com is to put a reverse proxy in front of GitHub Pages — most commonly by moving DNS management to Cloudflare (free tier) and using a Transform Rule or Worker to inject headers on the way through. This is a real infrastructure change, not a config toggle. See "If You Want Security Headers" below if you decide it's worth it.

Known Risks & Mitigations

Risk Level Mitigation Status
XSS via blog content Low Content originates from your own writing via the Claude API, not third-party input; rendered via MarkupString ✅ Acceptable
JSON file corruption (blog-posts.json) Low Versioned in git; restorable from history ✅ Mitigated
Repo access Low GitHub auth + local pre-push hook blocking direct pushes to main ✅ Mitigated
BackendTools API key exposure Low Never deployed; runs locally; key supplied via env var/user-secrets, not committed ✅ Mitigated
Accidental secret commit (.env, appsettings.Development.json) Low Explicitly gitignored as of 2026-08-10 (previously not excluded — no leak found, but the gap existed) ✅ Fixed
Missing HTTP security headers Medium Not achievable without a reverse proxy in front of GitHub Pages 🟡 Deferred — see below
No lead capture from "contact form" N/A (product, not security) mailto: link only ⚪ By design, unless changed

If You Want Security Headers (CSP / X-Frame-Options / HSTS)

This requires putting something in front of GitHub Pages that can rewrite the response. The lowest-effort path:

  1. Move DNS management for dougrosenbergdev.com to Cloudflare (free tier) — this means changing nameservers at your registrar to Cloudflare's, then re-adding your existing DNS records (A/CNAME to GitHub Pages, the Google Search Console TXT record, any email records) inside Cloudflare's dashboard.
  2. Enable Cloudflare proxying (orange cloud) on the record pointing at GitHub Pages.
  3. Add a Cloudflare Transform Rule (Rules → Transform Rules → Modify Response Header) to inject:
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    Referrer-Policy: no-referrer-when-downgrade
    Strict-Transport-Security: max-age=31536000; includeSubDomains
    Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://github-readme-stats.vercel.app
    
    (The CSP above is a starting point based on what this site actually loads — Bootstrap CDN, Google Fonts, GitHub stats iframes — not a copy-paste-and-forget value. Test it in report-only mode first.)

Is it worth it? For a static portfolio with no auth, no database, no user-submitted forms, and no third-party scripts beyond fonts/CDN CSS, the actual risk these headers mitigate is low. It's a reasonable "someday" item, not an urgent one. Re-evaluate if the site ever adds a real form, user accounts, or third-party embeds beyond what's here today.


Local Secrets Hygiene

  • ANTHROPIC_API_KEY (or AI:AnthropicApiKey in config terms) — set via environment variable or dotnet user-secrets, never committed. BackendTools/appsettings.json intentionally contains no secrets (just logging config).
  • .gitignore now excludes .env, .env.*, and appsettings.*.json (with appsettings.json itself explicitly un-ignored, since it's checked in and contains no secrets) — this protects against ever accidentally committing a real key to a per-environment settings file.

If You Ever Deploy BackendTools Publicly

Do not do this without:

  1. Adding authentication (Bearer token or OAuth)
  2. Rate limiting per IP
  3. Moving the API key to a real secrets manager appropriate to wherever you deploy it (this project has no Azure account, so "Azure Key Vault" isn't a given — pick whatever your actual host provides)
  4. HTTPS only
  5. Request logging for an audit trail

What's NOT Risky Here

  • ✅ SQL Injection — no database used
  • ✅ Unauthorized publishes — blog posts are static JSON, not executable, and only reach production via a PR merge to main
  • ✅ User authentication — no users, no auth needed
  • ✅ CSRF — no state-changing server endpoints exist to forge a request against

Dependency Updates

dotnet outdated
dotnet package update --interactive

Keep .NET, Bootstrap, MudBlazor, and all packages current. No fixed cadence has been established — worth doing whenever touching either project meaningfully.


Reporting Security Issues

This is a personal portfolio project. Security issues can be reported directly to doug.rosenberg@gmail.com.


Last updated: 2026-08-10 Next review: whenever the site's actual capabilities change (e.g. a real contact form, user accounts, third-party embeds)

There aren't any published security advisories