From 11cc8cbe2cb76a99f4bbe4a25c529fd86cb1d650 Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Mon, 18 May 2026 19:36:43 +0545 Subject: [PATCH 1/3] feat(middleware): expand security headers; drop X-XSS-Protection Adds CSP (enforcing, with SHA-256 hash for the single inline FOUC script), HSTS (max-age=31536000; includeSubDomains, no preload), Permissions-Policy denying 9 unused features incl. interest-cohort. Drops X-XSS-Protection (deprecated; Chrome removed support, Firefox never had it). CSP_DISABLE env var skips the CSP header for operators whose edge proxy emits its own. Regression tests lock the FOUC script hash against base.html and assert exactly one inline JavaScript across templates (JSON-LD excluded). --- .env.example | 10 +++ docs/configuration.md | 23 +++++++ internal/commands/serve/command.go | 2 +- internal/config/config.go | 13 ++++ internal/middleware/middleware.go | 54 +++++++++++++++- internal/middleware/middleware_test.go | 86 +++++++++++++++++++++++++- 6 files changed, 182 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 641cd37..75cd168 100644 --- a/.env.example +++ b/.env.example @@ -173,6 +173,16 @@ AMA_FORM_PLACEHOLDER=What would you like to know? AMA_SUBMIT_LABEL=Submit Question AMA_THANKYOU_COPY=Question submitted! It will appear once answered. +# ============================================================================= +# SECURITY HEADERS +# ============================================================================= + +# Set CSP_DISABLE=true when an edge proxy (Caddy, nginx, Cloudflare) emits its +# own Content-Security-Policy and you want to avoid double-headers. The other +# security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, +# Strict-Transport-Security, Permissions-Policy) always ship. +CSP_DISABLE=false + # ============================================================================= # LOGGING CONFIGURATION # ============================================================================= diff --git a/docs/configuration.md b/docs/configuration.md index 9d603de..5e055e4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -238,6 +238,29 @@ Operator-controllable copy on the AMA (Ask Me Anything) submission overlay. All | `AMA_SUBMIT_LABEL` | `Submit Question` | Label on the submit button on both the AMA sheet and the `/about` reach section (v3.14.0+). | | `AMA_THANKYOU_COPY` | `Question submitted! It will appear once answered.` | Toast shown after a successful submission. | +## Security Headers (v3.16.0+) + +markgo emits a fixed set of security headers on every response: + +| Header | Value | Notes | +|--------|-------|-------| +| `X-Content-Type-Options` | `nosniff` | Disables MIME sniffing. | +| `X-Frame-Options` | `DENY` | Disallows embedding in frames. | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Sends origin-only on cross-origin requests. | +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | One-year HSTS. The `preload` directive is intentionally omitted; preload-list registration is irreversible and only safe after observed stability. | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=(), interest-cohort=()` | Denies powerful features the app does not use, including FLoC opt-out. | +| `Content-Security-Policy` | See below | Enforced by default; disable via `CSP_DISABLE=true` when an edge proxy emits its own. | + +**CSP value:** `default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; object-src 'none'; script-src 'self' 'sha256-...'; style-src 'self'`. The `script-src` hash covers the single inline FOUC-prevention script in `base.html`; any new inline executable script needs a hash entry (regression test enforces this). + +`img-src` includes `https:` so article banner fields accept absolute URLs to externally-hosted images. JSON-LD blocks (``) + match := re.FindSubmatch(data) + require.NotNil(t, match, "expected exactly one inline ) + jsonLD := regexp.MustCompile(`]*type="application/ld\+json"[^>]*>`) + count := 0 + err := fs.WalkDir(web.Assets, "templates", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".html") { + return nil + } + data, err := web.Assets.ReadFile(path) + if err != nil { + return err + } + inlineJS := len(scriptOpen.FindAll(data, -1)) - len(jsonLD.FindAll(data, -1)) + if inlineJS > 0 { + t.Logf("inline JS in %s: %d", path, inlineJS) + } + count += inlineJS + return nil + }) + require.NoError(t, err) + assert.Equal(t, 1, count, + "expected exactly one inline JavaScript across templates (the FOUC script in base.html); "+ + "any new inline executable script needs a CSP hash entry — see foucScriptHash in middleware.go") +} + +// TestSecurity_CSPDisableSkipsCSPHeader verifies CSP_DISABLE=true skips the CSP +// header without affecting the other security headers. +func TestSecurity_CSPDisableSkipsCSPHeader(t *testing.T) { + router := setupTestRouter() + router.Use(Security(&config.Config{Security: config.SecurityConfig{CSPDisable: true}})) + router.GET("/test", func(c *gin.Context) { c.String(200, "ok") }) + + req := httptest.NewRequest("GET", "/test", http.NoBody) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Empty(t, w.Header().Get("Content-Security-Policy")) + assert.Equal(t, "max-age=31536000; includeSubDomains", w.Header().Get("Strict-Transport-Security")) + assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) + assert.Contains(t, w.Header().Get("Permissions-Policy"), "interest-cohort=()") } // TestLogger tests the logger middleware From ee17b058d6731c34301963a5e6139ce4af39ef71 Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Mon, 18 May 2026 19:37:11 +0545 Subject: [PATCH 2/3] docs(readme): point live install at log.1mb.dev --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a09b26d..b19f34b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ make build Or download a release from [GitHub Releases](https://github.com/1mb-dev/markgo/releases). +## Live Install + +[log.1mb.dev](https://log.1mb.dev) — running latest release (may briefly lag during binary-pull cycles). + ## What You Get **Write from anywhere** — CLI for drafting in your editor, web compose form for publishing from your phone. Quick capture: tap the FAB, type a thought, hit Publish. Under 5 seconds. From 65095628872fc1ac2a7006ae7b3850c3c4395a1a Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Mon, 18 May 2026 19:38:54 +0545 Subject: [PATCH 3/3] docs(audit): catalog v3.16.0 frontend audit findings + v3.16.0 release notes 12 deferred items filed to docs/audit-2026-05-frontend.md as a tracked index; each gets a GitHub issue (audit-finding label) post-merge. CHANGELOG v3.16.0 entry consolidates security headers + Live install README + audit-doc additions. --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++ docs/audit-2026-05-frontend.md | 32 ++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 docs/audit-2026-05-frontend.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 89cec55..b7ff55f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.16.0] - 2026-05-18 + +Theme: **security headers + demo retire.** First user-driven scoping pass — +no anchoring issue. Closes two broken windows: the Pages demo had been +serving 96-day-stale wrong-account content, and the Security middleware +was shipping deprecated `X-XSS-Protection` while missing CSP / HSTS / +Permissions-Policy. A consolidated frontend audit doc catalogs deferred +items for v3.17.0+ cycles. + +### Added + +- **Content-Security-Policy** (enforcing) with a hardcoded SHA-256 hash + for the single inline FOUC-prevention script in `base.html`. + Directives: `default-src 'self'; base-uri 'self'; connect-src 'self'; + font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src + 'self' data: https:; object-src 'none'; script-src 'self' 'sha256-...'; + style-src 'self'`. JSON-LD blocks are unaffected (non-JS MIME). +- **Strict-Transport-Security**: `max-age=31536000; includeSubDomains`. + The `preload` directive is intentionally omitted; opt-in deferred to a + later cycle after observed stability. +- **Permissions-Policy**: denies camera, microphone, geolocation, payment, + USB, magnetometer, gyroscope, accelerometer, and FLoC (`interest-cohort`). +- **`CSP_DISABLE` env var** (default `false`): set to `true` when an edge + proxy emits its own CSP and you want to avoid double-headers. Other + security headers always ship. +- **Regression tests** lock the FOUC script hash to `base.html` contents + and assert exactly one inline JavaScript across templates — a future + inline `