v1.0.3 — Security hardening #93

Merged
claude-bot merged 7 commits from fix/v1.0.3-security into main 2026-07-18 07:29:27 +00:00
Contributor

Patch release closing the security findings from the 2026-07-17 codebase audit. Behaviour-preserving where possible.

Fixes

Issue Finding Fix
#45 F-06 Placeholder SECRET_KEY passed production validation (forgeable sessions). Rejected in every environment; .env.example ships empty with generation instructions; dev uses an ephemeral key + warning. Fernet derivation left unchanged to avoid breaking stored AI keys.
#46 F-07 voip.ms credentials leaked via GET query string (httpx INFO logs) and persisted error strings. Switched to POST body; added a central scrub_secrets() on all persisted notifier errors; quieted httpx request logging.
#47 F-08 Channel config secrets (tokens, webhook url/headers, phone numbers) were readable by any authenticated user incl. read-only. Redacted to has_<key> in list/non-manager responses; full config only for managers.
#51 F-12 Webhook validation ignored DNS. Now resolves all addresses and rejects private/loopback/link-local/reserved results; send-time connection pinning to the validated IP (Host/SNI preserved) closes DNS-rebinding.
#59 F-20 htmx / htmx-SSE / Leaflet now vendored under static/vendor/ instead of unpkg — no third-party script trust or availability coupling.
#60 F-21 Public page no longer shows exact coordinates; the NWS-alert public route honors public_enabled; disabling public sharing revokes the location's NWS tokens immediately.
#70 F-31 OIDC nonce generated/validated; logout is CSRF-protected POST (GET can't log out); media cache-key validation + path containment; excluded_event_types persisted on create.

Testing

Full suite green on the dev server (all deps): 417 passed, including new regression tests for placeholder-key rejection, credential scrubbing, per-channel secret redaction, DNS-resolution/connection-pinning SSRF, public-surface hardening, and the API bundle (nonce, logout CSRF, media validation, token revoke-on-unshare).

Reconciliation notes (cross-cutting)

  • Logout flow: backend POST /auth/logout (CSRF-protected) clears the session and returns the end-session URL as JSON; the frontend fetch()es it and navigates top-level — a fetch() can't follow a cross-origin 303 to Authentik, so JSON hand-off is used to actually terminate SSO.
  • Webhook url added to the redaction set (not just headers) since webhook URLs commonly embed a token.
  • Static dir: assets vendored under repo-root static/vendor/ (the actual StaticFiles mount), not app/static/.

Operational follow-up

⚠️ The voip.ms API password should be rotated after this deploys — it may already be present in historical logs/DB.

Closes #45, #46, #47, #51, #59, #60, #70

🤖 Generated with Claude Code

Patch release closing the security findings from the 2026-07-17 codebase audit. Behaviour-preserving where possible. ## Fixes | Issue | Finding | Fix | |---|---|---| | #45 | F-06 | Placeholder `SECRET_KEY` passed production validation (forgeable sessions). Rejected in every environment; `.env.example` ships empty with generation instructions; dev uses an ephemeral key + warning. Fernet derivation left unchanged to avoid breaking stored AI keys. | | #46 | F-07 | voip.ms credentials leaked via GET query string (httpx INFO logs) and persisted error strings. Switched to POST body; added a central `scrub_secrets()` on all persisted notifier errors; quieted httpx request logging. | | #47 | F-08 | Channel `config` secrets (tokens, webhook url/headers, phone numbers) were readable by any authenticated user incl. read-only. Redacted to `has_<key>` in list/non-manager responses; full config only for managers. | | #51 | F-12 | Webhook validation ignored DNS. Now resolves all addresses and rejects private/loopback/link-local/reserved results; send-time connection pinning to the validated IP (Host/SNI preserved) closes DNS-rebinding. | | #59 | F-20 | htmx / htmx-SSE / Leaflet now vendored under `static/vendor/` instead of unpkg — no third-party script trust or availability coupling. | | #60 | F-21 | Public page no longer shows exact coordinates; the NWS-alert public route honors `public_enabled`; disabling public sharing revokes the location's NWS tokens immediately. | | #70 | F-31 | OIDC `nonce` generated/validated; logout is CSRF-protected POST (GET can't log out); media cache-key validation + path containment; `excluded_event_types` persisted on create. | ## Testing Full suite green on the dev server (all deps): **417 passed**, including new regression tests for placeholder-key rejection, credential scrubbing, per-channel secret redaction, DNS-resolution/connection-pinning SSRF, public-surface hardening, and the API bundle (nonce, logout CSRF, media validation, token revoke-on-unshare). ## Reconciliation notes (cross-cutting) - **Logout flow**: backend `POST /auth/logout` (CSRF-protected) clears the session and returns the end-session URL as JSON; the frontend `fetch()`es it and navigates top-level — a `fetch()` can't follow a cross-origin 303 to Authentik, so JSON hand-off is used to actually terminate SSO. - **Webhook `url`** added to the redaction set (not just `headers`) since webhook URLs commonly embed a token. - **Static dir**: assets vendored under repo-root `static/vendor/` (the actual `StaticFiles` mount), not `app/static/`. ## Operational follow-up ⚠️ The **voip.ms API password should be rotated** after this deploys — it may already be present in historical logs/DB. Closes #45, #46, #47, #51, #59, #60, #70 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The shipped .env.example key (43 chars, not in the weak-values set) passed
validate_runtime() even under APP_ENV=production, so a default deployment ran
with a publicly-known session-signing key. Reject the placeholder and any
changeme* value in every environment (fail closed at startup); ship an empty
SECRET_KEY= with generation instructions; in development, generate an ephemeral
key with a warning when none is set. The Fernet-key derivation is left unchanged
to avoid breaking already-encrypted AI provider keys.

Closes #45

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Credentials were sent as GET query parameters (logged by httpx at INFO on every
send) and embedded in the error strings persisted to the diagnostics table. The
voip.ms call now POSTs credentials in the body; a scrub_secrets() pass strips
URLs/credential params from persisted notifier errors (applied centrally in
record_notifier_delivery, so all channels benefit); and httpx request logging is
lowered to WARNING. The voip.ms API password should be rotated operationally.

Closes #46

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Channel config JSONB (Webex bot_token, Pushover keys, webhook url/headers, SMS
and Signal phone numbers) was serialized verbatim and readable by any
authenticated user, including read-only, on global channels. Secret-bearing keys
are now redacted to has_<key> indicators in list and non-manager responses; full
config is returned only to callers who can manage the channel, preserving the
edit flow.

Closes #47

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
validate_webhook_url only checked literal IPs and a static hostname blocklist, so
a public-looking hostname resolving to RFC1918/loopback/link-local space was
accepted at both store and send time. Validation now resolves all A/AAAA records
and rejects any private/loopback/link-local/reserved/non-global result (unwrapping
IPv4-mapped IPv6); the generic webhook send pins the connection to the validated
IP while preserving the Host header and TLS SNI, closing the DNS-rebinding gap.

Closes #51

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vendor htmx, the htmx SSE extension, and Leaflet under static/vendor/ instead of
loading them from the unpkg CDN (removes third-party script trust and the
availability dependency on every page). Remove the exact-coordinate display from
the public location page. Convert the logout control to a CSRF-protected POST via
the app's fetch()+X-CSRF-Token convention, navigating top-level to the end-session
URL returned by the backend.

Refs #59, #60, #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- OIDC login now generates and validates a nonce, binding the ID token to the
  login request (replay/injection defense).
- Logout is a CSRF-protected POST that clears the session and returns the
  end-session URL as JSON; a GET can no longer terminate a session.
- The media route validates its cache key (^[A-Za-z0-9_-]+$) and enforces
  path containment within the radar cache dir.
- create_location now persists excluded_event_types (previously accepted by the
  schema and dropped).
- The NWS-alert public route honors Location.public_enabled, and disabling public
  sharing revokes the location's NWS public tokens immediately.

Closes #70, #60

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add v1.0.3 changelog entry
All checks were successful
CI / test (pull_request) Successful in 3m45s
59377f199d
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude-bot deleted branch fix/v1.0.3-security 2026-07-18 07:29:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/WeatherBot!93
No description provided.