Skip to content

Require a credential on loopback while the web dashboard is LAN-exposed (#1649) - #1657

Merged
erikdarlingdata merged 1 commit into
devfrom
feature/1649-loopback-token-in-network-mode
Jul 25, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
feature/1649-loopback-token-in-network-mode

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Closes #1649. Implements the option Erik picked: mirror the MCP host — in exposed mode even a local client presents the token.

The defect

DecideWebAuth passed 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 with web.network enabled, anything that could 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 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:

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 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:

var isLoopback = DarlingWebEndpoints.IsLoopbackRemote(remoteIp);

if (!isLoopback && !DarlingHostBinding.IsRemoteAddressAllowed(remoteIp, allowedCidr))
{
    return WebAuthAction.Forbid;
}
// ...falls through to the cookie / token checks like any other remote

The CIDR exemption has to stay: 127.0.0.1 is not inside a LAN CIDR like 10.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), so DecideWebAuth is 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:

  • The class doc and DecideWebAuth summary 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 is DecideWebAuth; the actual mutation gate is the Content-Type/CSRF check.
  • Two comments in DarlingCustomViewsTests called it the "tokenless-auth arm".

Test plan

The route matrix pins the behavior change directly — same loopback inputs, ShowLogin instead of Allow:

Remote Cookie Token Before After
127.0.0.1 no no Allow ShowLogin
::1 no no Allow ShowLogin
::ffff:127.0.0.1 no no Allow ShowLogin
127.0.0.1 yes no — Allow
127.0.0.1 no yes — SetCookieAndRedirect
::ffff:127.0.0.1 no yes — SetCookieAndRedirect

The 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.

  • Darling.Tests: 3065 passed, 0 failed (147 gated-live skipped) — up from 3062, and no pre-existing test depended on the old tokenless behavior.
  • Full solution builds 0 warnings, 0 errors.

🤖 Generated with Claude Code

…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>
@erikdarlingdata
erikdarlingdata merged commit eefdc92 into dev Jul 25, 2026
4 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/1649-loopback-token-in-network-mode branch July 25, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant