Skip to content

DECISION NEEDED: web dashboard's tokenless-loopback allow now covers write endpoints #1649

Description

@erikdarlingdata

Found by the security review in the 2026-07 maintenance pass (#1643). This one needs a product decision from Erik — no code change made.

The situation

DarlingWebHostService.DecideWebAuth (:540-547) returns Allow for any loopback remote before any token check, and both the class doc (:44-50) and the method justify it with: "the web surface is read-only."

Since Custom Views v2 that is no longer accurate. The surface now has:

  • DarlingWebEndpoints.cs:183 — POST (create)
  • :223 — PUT (update)
  • :267 — DELETE
  • :291 — compose/run (arbitrary composed query)

The MCP host went the other way on the identical question (DarlingMcpHostService.cs:508-509):

NO loopback exemption — in exposed mode even a local client must present the token; that IS the loopback guard against SSRF/sandboxed sockets.

So the two surfaces now diverge on the same reasoning, and the web side's stated basis no longer holds.

Failure scenario

Any local process on the Darling host — an unprivileged user account, SSRF'd code, a sandboxed process, a scheduled task — reaches http://127.0.0.1:5153 with no credential and can read the entire monitoring store and create, modify, or delete custom views.

This is distinct from the accepted "loopback MCP servers exist" pattern: here the loopback allow persists even while the host is LAN-exposed, and the surface mutates state.

The decision

  1. Narrow: require the token on loopback whenever networkMode is true, mirroring MCP. Loopback-only installs (the default) are unaffected.
  2. Broader: gate only the four mutation routes behind the token, leaving reads tokenless on loopback.
  3. Accept as-is: single-operator box, treat local processes as trusted.

Either of the first two also needs the doc comments at :44-50 and :534-539 corrected — as written they tell the next reader no writes exist.

Related, lower severity (same file)

Access token and session cookie cross the LAN in cleartext once network mode is on: the login form is method='get' (:696-737), so the long-lived access token becomes a URL query parameter on every first login, and the session cookie sets Secure = false (:624-637). The CIDR allowlist is a routing control and does not stop passive capture on the allowed segment. The 302 token-strip (:434-439) protects browser history and Referer, but not the wire.

Minimum: switch the login form to method='post' so the token stays out of URLs and intermediate access logs, and log a Warning at network-mode startup naming the cleartext exposure. TLS termination (or a documented, checked reverse-proxy requirement) is the actual fix.

Activity

  1. added a commit that references this issue on Jul 25, 2026
  2. erikdarlingdata commented on Jul 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Decided and implemented: option 1 — require the credential on loopback when network mode is on, mirroring the MCP host. Fixed in #1657, merged to dev as eefdc922.

    Loopback keeps the CIDR exemption (127.0.0.1 is not inside a LAN CIDR, so testing it there would 403 the operator's own browser) but no longer skips the credential — it takes the same token-to-cookie exchange a LAN client takes.

    Two things worth recording for whoever reads this next:

    • No signature change was needed. The auth middleware is registered inside if (networkMode), so DecideWebAuth is unreachable in loopback-only mode. Non-exposed dashboards are untouched and stay frictionless; the cost is one login on a host you deliberately exposed, then a 12-hour cookie.
    • Simply deleting the loopback arm would have been wrong — loopback would then fall to the CIDR test and get a flat 403 instead of a login prompt.

    The comments that allowed this to drift were corrected as part of the fix, including one asserting a second IsLoopbackRemote caller that kept the IPv4-mapped unwrap in sync. No such caller exists — DecideWebAuth is the only one, and the real mutation gate is the Content-Type/CSRF check. Left as-is, that comment would have told the next reader the invariant was already covered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions