Repository navigation
fix: harden the loopback surfaces a session exposes - #1067
Merged
Merged
Conversation
Three small fixes on the local boundaries SECURITY.md names. The preview server (internal/preview) served any Host on a predictable port, so a page in the user's ordinary browser could rebind a name it controls to 127.0.0.1 and read the checkout, .env included. It now answers only 127.0.0.1:<port> and localhost:<port>. The restart chain lich pastes after a self-update expanded $LICH_TOKEN into curl's argv, readable by every user through /proc/<pid>/cmdline. The transport accepts Authorization: Bearer <token> beside ?token=, and the chain, install.sh and native/slice.sh feed the header through curl's stdin config instead. The appupdate tests change because the contract (the pasted command) changed. lich's config directory is made 0700 on every start, existing installs included; the parent is left alone and a chmod failure is logged.
omartelo
added a commit
to lichdotdev/lich-plugin
that referenced
this pull request
Nov 8, 2026
Every hook script posted to "http://127.0.0.1:$LICH_PORT/<endpoint>?token=$LICH_TOKEN", so the expanded token was an argument of curl, readable by every user on the machine through /proc/<pid>/cmdline each time a hook fired. The scripts now send it as an Authorization: Bearer header fed to curl as a config line on stdin (printf is a shell builtin, so the token enters no argv; a -H argument would leak the same way). This is the shape lich adopted in lichdotdev/lich#1067, which makes the listener accept the header. A lich released before that answers 401 to a report whose token is only in the header. The scripts read the status of the first request and, on a 401 alone, send the report again with ?token=, so a card keeps its state under any lich. A 500 or a refused connection is not retried.
omartelo
added a commit
that referenced
this pull request
Nov 9, 2026
…ontract (#1069) #1067 made the loopback listener accept Authorization: Bearer beside ?token=, a contract change lich-plugin ships as 0.21.0 (its hooks send the header). The ceiling stayed at 0.21.0, so no lich would install that release. The compat tests move with the contract: 0.21.0 is now in range and 0.22.0 is the first release past it.
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.
What
Three fixes on the local boundaries
SECURITY.mdnames.Preview server accepted any Host (DNS rebinding).
internal/previewserves a session's checkout on a predictable port (25000-25999) and answered anyHost, so a page in the user's ordinary browser could rebind a name it controls to127.0.0.1and read the checkout,.envincluded. The server now answers only127.0.0.1:<port>andlocalhost:<port>(403 otherwise). The only caller is the session browser lich drives, which loads the URLpreview.URLbuilds with127.0.0.1:<port>.Token on a command line.
restartChainininternal/appupdatepastedcurl ... "/restart?token=$LICH_TOKEN", which expands the token into curl's argv, readable by every user on the machine through/proc/<pid>/cmdline.authorizedininternal/terminal/transport.gonow also acceptsAuthorization: Bearer <token>(?token=keeps working for the lich-plugin hooks and the in-process Go clients). The chain,install.shandnative/slice.shfeed the header to curl as a config line on stdin:printfis a shell builtin, so the expanded token never enters an argv (-Hwould leak the same way). The PowerShell chain is in-process and unchanged.SECURITY.mdnow promises "never in a log, a crash report or a process's command line" instead of "a URL", since in-process clients legitimately use?token=. Theappupdatetests that pin the pasted command change because the contract (the pasted command) changed.Config dir world-readable.
~/.config/lichwas created 0755 and left as is on an existing install, withlich.db(prompts and session text) inside.store.opennow creates it 0700 and chmods it 0700 on every start, so existing installs are tightened too; the parent (~/.config) is never touched, and a chmod failure is logged, not fatal.Docs:
docs/ceilings.md(preview Host rule),docs/hooks/README.mdanddocs/cli.md(Bearer beside?token=),CHANGELOG.mdunder### Security.Test plan
TestServedCheckoutRefusesAForeignHost: served 200 toHost: evil.examplebefore the fix, 403 after;127.0.0.1:<port>,localhost:<port>andLOCALHOST:<port>still 200; bare127.0.0.1, a wrong port and127.0.0.1.evil.testrefused.TestRestartAcceptsABearerToken: 401 before, 204 after; a wrong Bearer stays 401. Existing?token=tests unchanged and green.TestOpenMakesTheDatabaseDirectoryPrivate: a pre-existing 0755lich/becomes 0700 on open, its parent stays 0755.PATHrecording curl's argv: all four deliveredAuthorization: Bearer <token>, and the token appeared in none of the argv lines.gofmt -l .empty,go vet ./...,go test ./...on the backend;gofmt,go vet,go testundernative/.