Repository navigation
Require a credential on loopback while the web dashboard is LAN-exposed (#1649) - #1657
Merged
Merged
Conversation
…ed (#1649) The browser auth gate passed every loopback request tokenless, justified in the code by "the web surface is read-only." That was true when written and stopped being true at Custom Views v2, which added view create/update/delete plus /api/compose/run. On a host with web.network enabled, anything able to open a socket to 127.0.0.1 -- a scheduled task, SSRF'd code in another service, a second user's session, a sandboxed process -- could read the whole monitoring store and mutate custom views presenting nothing at all. The MCP host had already answered this exact question the other way ("NO loopback exemption -- in exposed mode even a local client must present the token; that IS the loopback guard against SSRF/sandboxed sockets"), so two surfaces on the same box disagreed on the same threat. DecideWebAuth now mirrors MCP. Loopback is exempt from the CIDR test ONLY -- 127.0.0.1 is not inside a LAN CIDR, and testing it there would 403 the operator's own browser -- and still needs a session cookie or ?token=, taking the same token-to-cookie exchange a LAN client takes. No signature change was needed: the caller registers this middleware inside if (networkMode), so the method is unreachable in loopback-only mode and a non-exposed dashboard is untouched and stays frictionless. The cost is one login on a host you deliberately exposed, then a 12-hour cookie. A null/unverifiable remote still fails closed to Forbid (not loopback -> CIDR test -> Forbid), so that invariant is unchanged. Also corrected the comments that let this drift unnoticed: the class doc and DecideWebAuth summary still described the surface as read-only (and claimed ratification), the IsLoopbackRemote doc said "a loopback request needs no token" and referenced a second "mutation gate" caller that does not exist -- its only production caller is DecideWebAuth; the actual mutation gate is the Content-Type/CSRF check -- and two test comments called it the tokenless arm. Route matrix pins the change: same loopback inputs now ShowLogin instead of Allow, plus the loopback cookie and token paths and the IPv4-mapped ::ffff:127.0.0.1 form so the unwrap can never drift between callers. Darling.Tests 3065 passed / 0 failed; solution builds 0 warnings, 0 errors. Closes #1649 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 25, 2026
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 #1649. Implements the option Erik picked: mirror the MCP host — in exposed mode even a local client presents the token.
The defect
DecideWebAuthpassed every loopback request tokenless, on a rationale written into the code: "the web surface is read-only."That was true when it was written. It stopped being true at Custom Views v2, which added view create/update/delete plus
/api/compose/run. So on a host withweb.networkenabled, anything that could open a socket to127.0.0.1— a scheduled task, SSRF'd code in another service, a second user's session, a sandboxed process — could read the entire monitoring store and create, modify, or delete custom views while presenting nothing at all.The MCP host had already decided this identical question the other way:
So two surfaces on the same box disagreed about the same threat, and the web side's stated basis no longer held.
The change
Loopback is now exempt from the CIDR test only, never from the credential:
The CIDR exemption has to stay:
127.0.0.1is not inside a LAN CIDR like10.0.0.0/24, so simply deleting the loopback arm would have 403'd the operator's own browser instead of prompting for the token.No signature change was needed. The caller registers this middleware inside
if (networkMode), soDecideWebAuthis unreachable in loopback-only mode — a non-exposed dashboard has no gate to pass and is completely untouched. The cost of this change is one login on a host you deliberately exposed, then a 12-hour cookie, exactly as a LAN client gets.A null/unverifiable remote still fails closed to
Forbid(not loopback -> CIDR test ->Forbid), so that invariant is unchanged and its test still passes as written.Comments corrected
Stale comments are how this drifted unnoticed, so they are part of the fix rather than an afterthought:
DecideWebAuthsummary still described the surface as read-only, and claimed the exemption was ratified.IsLoopbackRemote's doc said "a loopback request needs no token", and referenced a second "mutation gate" caller keeping the IPv4-mapped unwrap in sync — that caller does not exist. Its only production caller isDecideWebAuth; the actual mutation gate is the Content-Type/CSRF check.DarlingCustomViewsTestscalled it the "tokenless-auth arm".Test plan
The route matrix pins the behavior change directly — same loopback inputs,
ShowLogininstead ofAllow:127.0.0.1::1::ffff:127.0.0.1127.0.0.1127.0.0.1::ffff:127.0.0.1The IPv4-mapped rows are deliberate: they prove the unwrap cannot drift between the loopback determination and the auth decision. Out-of-CIDR, in-CIDR, and null-remote rows are unchanged.
🤖 Generated with Claude Code