Repository navigation
fix(security): harden the admin PIN and refuse cross-site writes (mrramam/HomeGlow#62, #73) - #329
Merged
Merged
Conversation
…amam#62, #73) The admin PIN is tamper resistance for a household, and the README now says so plainly: HomeGlow is local-only, not secure, and not for the internet. - The PIN is stored as scrypt with a random salt and compared in constant time. An old unsalted SHA-256 is rehashed on its next correct entry. All PIN checks live in utils/adminPin.js. - Five wrong PINs, then a wait that doubles from 30 seconds to five minutes, even for the right PIN. One limit for every route that checks it (429 with Retry-After). - Staging a restore (file or Drive) and a rollback need the PIN, like a backup download already did. Cancel and restart don't: they can only drop or apply what the PIN allowed. - A write whose Origin is another website is refused (403), judged by Sec-Fetch-Site, or by host for browsers without it. CORS stopped other sites reading the API; this stops them posting to it. - The backup dialogs share one PIN prompt (useAdminPinPrompt), and every PIN prompt says how long to wait when locked (en, es). - Tests: the admin PIN routes on a real server (#73), the hashing and the limit, and the cross-site check. Co-Authored-By: Claude Opus 5.5 <[email protected]>
adamecker
pushed a commit
to adamecker/HomeGlow
that referenced
this pull request
Oct 26, 2026
… revision counter jherforth#339, dimming jherforth#334, Save layout jherforth#336, chore perf jherforth#324, admin jherforth#330/jherforth#331/jherforth#332, PIN hardening jherforth#329 Conflicts resolved taking upstream's rewrite, preserving fork(prod) features: - displayDim.js: upstream jherforth#334 base plus fork-only kiosk URL dim overrides and LCD pixel shift (both absent upstream) - ChoreWidget/app.jsx/ChoreSchedulesTab: upstream jherforth#324/jherforth#336 rework
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the rest of mrramam#62, and the admin PIN part of #73.
The framing, now in the README
HomeGlow is a local-only app for a trusted home network. It's not secure, and it isn't designed to be exposed to the internet. The admin PIN is tamper resistance against household members who shouldn't make changes, not access control. The README's Security section and
docs/README.mdnow say this up front. The reverse-proxy line no longer reads as an invitation to publish HomeGlow: if a proxy reaches the internet, it says to put a login in front of it.Changes
The PIN (
server/utils/adminPin.js)timingSafeEqual.verify, the backup routes and Home Assistant unlock actions.429withRetry-After, or the limit would only slow a guesser down;Routes that now check the PIN on the server
POST /api/backup/local/import(apinfield before the file, checked before the upload is written) andPOST /api/backup/drive/restore/stage.POST /api/backup/drive/restore/rollback.400when nothing is staged.Writes from other websites (
server/utils/crossSiteWrites.js)#320 stopped other sites from reading the API, but a browser still sends a form or multipart POST from any page. Any write (
POST/PUT/PATCH/DELETE) whoseOriginis another website now gets403.Sec-Fetch-Site, which the browser sets, so no proxy in between can get it wrong. An older browser without it is judged by hostname againstHost/X-Forwarded-Host. Hostnames only, because nginx'sHost $hostdrops the port.Origin(curl, Home Assistant, scripts);Origin: null), which the plugin policy already holds to their token;CORS_ORIGINS;Client
useAdminPinPrompt): download, restore from a file, roll back, stage a Drive restore, and download a backup's key. It checks the PIN, then closes the dialog and runs the action, so a long upload shows its progress on the page.Docs
backend-api.md,configuration.mdanddatabase.mddescribe the PIN format, the limit, the guarded routes and the cross-site rule.Not changed, on purpose
verifyand sent with every admin write. That's a larger change, and beyond tamper resistance for a household. The README now says plainly that the PIN doesn't stop someone who calls the API directly.Testing
Hostwithout the port, as nginx does, withNODE_ENV=production. 10 of 10 checks pass:403(Sec-Fetch-Site: cross-site), and a cross-site settings write isn't saved;same-origin);adminPinRoutes.test.js, a real server (Calendar view #73):setstores scrypt, andexistsnever shows the hash;verifytakes the right PIN and refuses a wrong or missing one;403, while same-origin and no-Origin writes don't;429onverifyand on the export;DELETEclears the PIN.All but the
verifytest fail onmain.adminPin.test.js: salting, legacy matching and rehashing, a missing PIN not counting, and the waits of 30, 60, 120, 240 and 300 seconds, capped.crossSiteWrites.test.js: cross-site, same-site on another port, behind a proxy, older browsers, and the exemptions.npm test(646), clientvitest(755),check:i18nandbuildpass.Left in #73
These aren't about the PIN:
refreshAccessTokentest for Google;pluginStore.test.jswriting intoserver/widgets/.🤖 Generated with Claude Code