hearth is archived. No updates, no patches. Whatever is wrong with it stays wrong.
So read this before you install it anywhere. I'd rather tell you what I checked and what I skipped than hand you a disclaimer and let you fill in the rest yourself.
There isn't a reporting path. Nobody is watching this repo, and an issue or a private advisory will sit unread.
Coordinated disclosure needs someone to coordinate with, and there isn't one. So if you find something:
- Fork it and fix it in your fork. That's what GPL-2.0 is for.
- Publish what you found. Other forks and anyone running this need to know, and sitting on it protects nobody.
- Stop running the affected version while you sort it out.
All of this was measured on the live router, not assumed.
The panel binds the LAN address only. Never 0.0.0.0, never the WAN address. WAN input is REJECT, the firewall's default input policy is REJECT, and SSH is key-only with password auth off for root and everyone else. You have to be on the LAN before any of it is reachable at all.
Authentication is rpcd's, not mine. No panel module checks a session itself, and none should: rpcd resolves acl/hearth.json before it ever spawns a plugin. That ACL is deny-by-default with no wildcards and no passthrough verb, and it enumerates exactly the methods the modules expose. An unauthenticated session gets session.login and session.access, nothing more.
But the hearth account is not a privilege boundary. rpcd runs every plugin as root, so an authenticated session has root-equivalent control of the router through the panel's own verbs. Its uid 1001 and /bin/false shell hold it to nothing over SSH and to nothing at all over /ubus. Treat the panel password as a root password.
Session tokens live in the browser tab's memory, never localStorage or sessionStorage, so a page reload drops them. Changing the admin password kills every live session, the one that changed it included.
Two bounded passes before publishing, both against the live router, both AI-assisted. A model did the reading at that scale; I drove it, and the findings were re-checked against the source and the running box before any of them was called fixed. This was not an independent audit and you shouldn't read it as one.
The first traced every argument interpolated into a shell command back to where it came from, across all 90 system() and popen() call sites in the ten backend modules, plus the call sites of the helpers some of them funnel through. The second checked that the ACL enumerates exactly the 71 methods the modules expose, that no write method escapes the session check, and how session tokens get issued, stored, and expired. What the two turned up is fixed in this snapshot.
Four things I left out, and I'm claiming nothing about any of them:
- CSRF. The session token sits in memory rather than a cookie, which makes the classic version harder, but I didn't design for it, test it, or verify it.
- The firmware flash and restore paths end to end. Individual checks in there got looked at (checksum gating, tar path and entry-type validation, staging the upload outside the ACL that can write to it). The paths as a whole did not.
- TLS and certificate handling. The panel serves a self-signed cert.
- WireGuard key material, beyond the injection question.
Known, unfixed, and some of it not fixable at the panel's level.
- Session lifetime is whatever the client asks for, because rpcd applies no ceiling. The panel requests 900 seconds at login, and that number in
frontend/src/lib/rpc.jsis a request rather than a bound. I asked a live box for a year and got it, capped only by the millisecond timer overflowing somewhere near 24.8 days. Nothing the panel does can constrain a session it didn't create, since anything at all can callsession.logindirectly. - Expiry is idle time, not elapsed time. Every session lookup refreshes the timer, and the panel polls its own status view, so an open browser tab keeps a session alive indefinitely. I watched a 20-second session still report 20 seconds remaining after two lookups 12 seconds apart, then die 25 seconds after the last one. Close the tab or sign out, and don't lean on the timeout.
- No rate limit on login attempts. rpcd doesn't offer one and I didn't write one. What guards the login is the LAN-only bind, the WAN reject policy, and key-only SSH, so it's worth exactly as much as your LAN is trustworthy.
- The 12-character password minimum only applies in-app. Change the password through the panel and it wants 12 characters, printable ASCII, no colon. Provisioning goes through
passwd hearth, where busybox grumbles about a weak password and then sets it anyway. Apply the minimum yourself. - Arming a confirm-or-revert window is check-then-act. Each write verb reads the pending gate on entry and writes its marker afterward, with no atomic acquire across the gap, so two overlapping calls can both pass the gate and both arm. Each watchdog then restores its own snapshot over the other's committed change. It takes two arming actions racing inside a very small window, which is why I accepted it on a one-admin box instead of fixing it. Run more than one admin session and it's there.
- The DHCP pool lookup keeps the last match, and dnsmasq gets addressed positionally as
@dnsmasq[0]. Fine on a single-instance box that lets the panel manage its own pools. Run several dnsmasq instances, or hand-add pools, and the panel may read or write the wrong one. - PPPoE credentials allow some shell metacharacters. They get written as uci values and never reach a shell inside hearth, but ppp's own scripts consume them downstream. Keep quotes and backslashes out of the password.
Read profiles/example-three-zone.sh and acl/hearth.json first. Between them they describe the whole authority model: what the panel can touch, and what it assumes about the network around it. The backend is about 4,300 lines of ucode across ten standalone files, and you can get through any one of them in a sitting.