v0.29.3
The Home Assistant sidebar panel showed a bare "404: Not Found".
Everything about it pointed away from the real cause. The panel was registered
correctly — get_panels showed local_zwave_tui with the same shape as the
working Power panel — and the add-on served every route it should
(/ → 302, /console → 200, /api/health → 200). The address bar stayed on
/local_zwave_tui throughout, so it read as a broken panel registration.
It was the redirect target. Home Assistant loads an ingress panel at
/api/hassio_ingress/<token>/, and the landing route replied
Location: /console — an ABSOLUTE path. The browser discards the ingress prefix
and asks Home Assistant itself for /console, which HA does not serve. Hence a
404 rendered inside an otherwise-healthy HA, from a redirect that had thrown its
own path away.
The rest of the console page was already ingress-safe — relative asset URLs
(./console/xterm.js) and a WebSocket URL derived from location.pathname.
This one line was not, and nothing covered it.
The fix reads X-Ingress-Path and prefixes the redirect. It lives in
auth.ts as ingressRedirectTarget() rather than inline at the route, so the
test and the mutant target the SAME code: the first version of the test
re-implemented the rule in the test file, which proves only that the rule is
self-consistent and would have let the mutant survive.
That header is attacker-controllable, and the first cut of this fix handled it
badly in three ways. CodeQL's gate caught one — js/polynomial-redos,
security-severity 7.5: trailing slashes were trimmed with /\/+$/, which
backtracks quadratically on a long run of / supplied by the caller. Reviewing
it turned up a worse one the scan did NOT flag: //evil.com is a
protocol-relative URL, so Location: //evil.com/console would have sent the
browser to another origin — an open redirect, introduced by the fix. A clean
scan is not the same as a safe input path.
The header is now validated rather than sanitised: rooted single slash only
(a second one disqualifies it), no CR/LF/backslash, a 256-character cap, and a
linear charCodeAt trim instead of a regex. Anything that does not look like an
ingress path falls back to /console — a redirect is not worth guessing at.
Direct (non-ingress) access on :8788 carries no header and still lands on
/console.