HTTP→HTTPS redirect does not cover the admin/session routes, so the dashboard login accepts a plaintext password #69
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
redirect_http_to_httpsdefaults totrueand the code treats suppressing it as an attack (see thetrust_forwardedcomment inproxy.rsabout sslstrip). But the redirect is implemented insideproxy::proxy_handler, which is mounted as the fallback inbuild_app(main.rs:116). Anything that matches a real route is handled before the fallback and therefore never redirected.That includes every admin/session route, among them
POST /api/session— the dashboard login, which carries the master key.Observed against v2.0.0 (
9ab9f83) on the devinfra deployment,HTTPS_PORT=443,redirect_http_to_httpsleft at its default:The 400 is just the empty test body being rejected — the point is the request was handled on the plaintext listener instead of being bounced. A real credential posted there goes over the wire in the clear, and the session cookie's
Secureflag won't be set on the response.Why this needs fixing in the product
It is closeable per-deployment by dropping the plaintext listener from
[[admin.access]]—admin_ip_middlewareevaluatesaccess_allowedagainst the listener (middleware.rs:151), so the route then 403s on http. The browser flow is unaffected, becauseGET /isn't a registered route and still falls through to the redirect.But that requires every deployment to know about the gap and remember to close it in its own routing table, while the config it's reading tells it the redirect is on. The secure-by-default posture is doing the opposite of what it claims for exactly the routes where it matters most.
Suggested direction
Either:
(1) is the general fix; (2) is worth having either way as a backstop.
Mitigating context for the deployment that found it
On devinfra the plaintext port is loopback-only (
docker.expose_ports_to_lan=false), and a browser never takes this path — it is redirected before the login form renders. So the practical exposure there is low and this is not urgent for that machine. The bug is general, though, and a deployment that does expose port 80 would be handing out the master key in cleartext to anything that skips the redirect.Fixed in
78ae254by option (2), not option (1). Both your observations were correct against the code, including that dropping the plaintext listener from[[admin.access]]closes it per-deployment —/api/sessionis behindadmin_ip_middleware, unlike the registration routes in #55.Why (2) rather than the general fix. Layering the redirect over the whole app would
301/healthand/ready, which are matched routes served on plaintext today. A probe that does not follow redirects, or follows one to a cert it does not trust, goes red — so (1) as written trades a credential leak for a monitoring outage. It is raised as #70 with the exemption to design in.(2) is also strictly stronger for this route: it holds where an operator sets
redirect_http_to_https = false, which is exactly the case a redirect cannot cover.What landed:
POST /api/sessionand/api/session/logoutreturn403unless the request reached the client over TLS./healthshares that router and is deliberately exempt, so the guard sits on a sub-router holding only the two session routes.health_is_still_served_on_plaintextfails if it moves up a level — verified by putting it there on purpose.X-Forwarded-Proto, while the cookie'sSecureflag checked only the marker. One trust-gatedaccess::request_is_httpsnow serves the redirect, the guard and the cookie.The discriminating test is a forged
X-Forwarded-Proto: httpson an untrusted listener being refused — a bare header sniff passes the plaintext and trusted-proxy cases identically.Narrow breaking case, in the v2.1 notes: TLS terminating upstream on a listener without
trust_forwarded_for, plusredirect_http_to_https = false. Login returns403there until that listener is marked trusted.One correction to your report. It says
HTTPS_PORT=443, redirect_http_to_https left at its defaultfor devinfra — that is right, and it corrects me: I had claimed inc8c6382's message that devinfra's redirect never fires, from reading its TOML without its compose env. Thank you for the precise reproduction; it is what caught my error.Follow-ons: #70 (app-wide redirect) and #71 (API-key routes on plaintext, same class, wider blast radius). Neither started.