From 26f0424ae3c63ea7e45093101bb5fa929ee845f1 Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Tue, 21 Apr 2026 18:16:42 +0200 Subject: [PATCH] fix: auth-guard / and /settings, add Logout link to static navs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since 26.11 shipped login, two of the three nav pages were secretly unauthenticated. The Caddyfile only reverse-proxied /api/*, /apps*, /login*, /logout* to the Python auth-gated handler. Everything else — including / (landing page) and /settings/ — fell through to Caddy's catch-all file_server straight out of assets/www/, skipping the session check entirely. LAN visitor effect: they could read the box's hostname, IP, Furtka version, uptime, and see all the Update-now / Reboot / HTTPS-toggle buttons on /settings/. The API calls those buttons fired were themselves 401-gated so nothing actually happened — but the info leak plus "looks open" UX was real. Caught in the 26.13 SSH test session when the user noticed Logout only appeared in the nav on /apps, and not on / or /settings/. Fix: - Caddyfile: new `handle /settings*` and `handle /` blocks in the shared `(furtka_routes)` snippet reverse-proxy to localhost:7000, so both hit the Python auth-guard before the HTML goes out. - api.py: new `_serve_static_www(relative_path)` helper reads assets/www/{index.html, settings/index.html} with a path-traversal clamp (resolved path must stay under static_www_dir). `do_GET` routes `/` and `/settings[/]` to it. Removed the `/` branch from the old combined-with-/apps line — those are different pages now. - paths.py: new `static_www_dir()` helper with `FURTKA_STATIC_WWW` env override for tests. - assets/www/*.html: both nav bars get the Logout link + a shared `doLogout()` inline script matching the _HTML pattern. Users never see the link unauthed (the Python handler 302s them before the page renders), but authed users get consistent navigation across all three pages. Tests: 5 new cases in test_api.py — unauth / redirects, unauth /settings redirects (both trailing-slash and not), authed / serves index.html, authed /settings serves settings/index.html, regression guard that / and /apps serve different content. Existing test updated (the one that used / as a proxy for /apps). Static /style.css, /rootCA.crt, /status.json, /furtka.json, /update-state.json stay served by Caddy's catch-all — those are public by design (login page needs style.css, fresh users need the CA to trust HTTPS, runtime JSON is metadata not creds). 272 tests pass, ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 31 ++++++++++++- assets/Caddyfile | 14 ++++++ assets/www/index.html | 12 +++++ assets/www/settings/index.html | 10 ++++ furtka/api.py | 37 ++++++++++++++- furtka/paths.py | 8 ++++ pyproject.toml | 2 +- tests/test_api.py | 84 +++++++++++++++++++++++++++++++++- 8 files changed, 193 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90da56b..e1c72d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r ## [Unreleased] +## [26.14-alpha] - 2026-04-21 + +### Fixed + +- **Landing page and `/settings/` were silently bypassing the auth + guard.** Since 26.11 shipped login, the Caddyfile only + reverse-proxied `/api/*`, `/apps*`, `/login*`, and `/logout*` to + Python. Everything else — including `/` and `/settings/` — fell + through to Caddy's catch-all `file_server` and was served straight + from `assets/www/` without ever hitting the session check. The + effect: a LAN visitor saw the box's hostname, IP, Furtka version, + and the buttons for Update-now / Reboot / HTTPS-toggle. The API + calls those buttons fired were all 401-auth-gated so actions didn't + land, but the information leak and the "looks open" UX was a real + bug. Caught in the 26.13 SSH test session when the user noticed + Logout only showed up on `/apps`. Now Caddy routes `/` and + `/settings*` through Python; a new `_serve_static_www` handler + checks the session cookie, redirects to `/login` if unauthed, and + reads the HTML from `assets/www/` otherwise. Catch-all still + serves `/style.css`, `/rootCA.crt`, and the runtime JSON files + publicly — those don't need auth. +- **Logout link now shows on every authed page, not just `/apps`.** + The static HTML for `/` and `/settings/` maintained their own nav + separate from `_HTML` in `api.py`, so they never got the Logout + entry when it was added in 26.11. Both nav bars now include it + plus an inline `doLogout()` that POSTs `/logout` and bounces to + `/login`, matching the pattern in `_HTML`. + ## [26.13-alpha] - 2026-04-21 ### Fixed @@ -279,7 +307,8 @@ First tagged snapshot. Pre-alpha — the installer does not yet boot, but the de - **Containers:** Docker + Compose - **License:** AGPL-3.0 -[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.13-alpha...HEAD +[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.14-alpha...HEAD +[26.14-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.14-alpha [26.13-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.13-alpha [26.12-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.12-alpha [26.11-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.11-alpha diff --git a/assets/Caddyfile b/assets/Caddyfile index 36cc8ea..b9437af 100644 --- a/assets/Caddyfile +++ b/assets/Caddyfile @@ -41,6 +41,20 @@ handle /logout* { reverse_proxy localhost:7000 } + # /settings and / — these previously served as static HTML straight + # from the catch-all file_server, which meant the auth-guard was + # bypassed: a LAN visitor could see the box's version, IP, and + # reach the Update-now / Reboot buttons (the API calls behind them + # are auth-gated, but the page itself rendered without a redirect + # to /login). Route them through the Python handler which checks + # the session cookie and either serves the static HTML from + # assets/www/ or redirects to /login. + handle /settings* { + reverse_proxy localhost:7000 + } + handle / { + reverse_proxy localhost:7000 + } # Runtime JSON lives under /var/lib/furtka/ so it survives self-updates # (which only swap /opt/furtka/current). handle /status.json { diff --git a/assets/www/index.html b/assets/www/index.html index 6bf3476..66e5e2c 100644 --- a/assets/www/index.html +++ b/assets/www/index.html @@ -14,6 +14,7 @@ Home Apps Settings + Logout
@@ -67,6 +68,17 @@