Skip to content

fix: answer the loopback under every one of its names - #55

Open
lgnap wants to merge 1 commit into
antoinevalentinHA:masterfrom
lgnap:fix/cors-loopback-origin
Open

lgnap wants to merge 1 commit into
antoinevalentinHA:masterfrom
lgnap:fix/cors-loopback-origin

Conversation

@lgnap

@lgnap lgnap commented Sep 9, 2026

Copy link
Copy Markdown

The web UI loses device monitoring the moment it is opened the way the application opens it. Reported from a browser, then reproduced with curl:

Origin: http://localhost:8080  -> answered
Origin: http://127.0.0.1:8080  -> 403 CORS Rejected - Invalid origin

Why

The CORS policy allowed http://localhost:.* and nothing else. It was written when the server bound every interface and was reached by name. Since it binds the loopback, listenHost() is an address rather than a name, so the URL openInBrowser is handed is http://127.0.0.1:8080.

A browser sends an Origin header on a POST even when it is same-origin, so the page the application had just opened was refused by the application that opened it. The event bus handshake is a POST, so every SockJS request answered 403, the channel never opened, and the UI retried for as long as the tab stayed open while telling the user monitoring was down. Nothing about it was intermittent: opened that way, it could not work at all.

The fix

The loopback is one host under several names, so localhost, 127.0.0.1 and [::1] are all accepted, with the port left open-ended because the development server picks its own.

Not widened to *. This API has no authentication, and whatever a page on another origin could reach here, it could reach in the browser of anyone who visits it. CorsOriginTest asserts that refusal with the same weight as the acceptance — a policy that allowed everything would pass the nominal cases on its own — including the near misses http://localhost.example.com:8080 and http://127.0.0.1.example.com:8080, which the pattern must not treat as the loopback.

Verification

The test deploys the real MainVerticle, like StaticResourceConfinementTest, because what is under test is the router's configuration rather than a handler in isolation. It failed first with the exact production response (HTTP/1.0 403 CORS Rejected - Invalid origin), then passed.

  • CorsOriginTest: 3 tests
  • Java suite: 91 tests in web-ui, 0 failures, BUILD SUCCESS across every module

🤖 Generated with Claude Code

The CORS policy allowed `http://localhost:` and nothing else. It was written when
the server bound every interface and was reached by name; since it binds the
loopback, `listenHost()` is an address rather than a name, so the URL the
application opens in the browser is `http://127.0.0.1:8080`.

A browser sends an `Origin` header on a POST even when it is same-origin, so the
page the application had just opened was refused by the application that opened
it. The event bus handshake is a POST: every SockJS request answered `403 CORS
Rejected - Invalid origin`, the channel never opened, and the UI retried for as
long as it stayed open while showing the user that device monitoring was down.
Nothing about it was intermittent -- it could not work at all -- and it was
reported from a browser before it was reproduced with curl.

The rule is that the loopback is one host under several names, so all three are
accepted and the port stays open-ended, which the development server needs. It is
not widened to `*`: this API has no authentication, and whatever a page on another
origin could reach here, it could reach in the browser of anyone who visits it.
That refusal is asserted with the same weight as the acceptance, since a policy
that allowed everything would pass the nominal cases on its own.
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