Skip to content

Reject foreign host names on the loopback server - #332

Merged
JuergenFleiss merged 5 commits into
aTrainTranscription:developfrom
BW-Projects:bugfixes_host_check
Nov 9, 2026
Merged

JuergenFleiss merged 5 commits into
aTrainTranscription:developfrom
BW-Projects:bugfixes_host_check

Conversation

@BW-Projects

@BW-Projects BW-Projects commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Once a website's domain resolves to 127.0.0.1 (DNS rebinding, #300), a page from that site could load and drive aTrain while it listens on loopback. The server did not check the Host header and NiceGUI accepts socket.io connections from every origin.

aTrain start --no-native used to bind to all interfaces when --host was not given. It now binds to 127.0.0.1 like the native window. When aTrain binds to 127.0.0.1 or localhost, the server now:

  • answers only to those host names, with Starlette's TrustedHostMiddleware, which also covers the socket.io mount
  • lets only pages from its own origin open socket.io (engineio's default, which NiceGUI overrides with *)

Serving on the network takes an explicit --host, for example 0.0.0.0. With any address other than 127.0.0.1 or localhost, aTrain behaves as before and checks neither host names nor origins. A reverse proxy on the same machine in front of a loopback-bound aTrain sends its public host name or origin, so such setups now need the new --allow-any-host.

Existing --no-native setups that serve on the network need --host 0.0.0.0 after the update. The Linux service guide now sets it. The README and the service guide warn that this mode has no check. The CHANGELOG lists the check, --allow-any-host and the new default.

The browser e2e tests start the app with plain --no-native, so they run through the check with the default address.

References

Refs #300. The issue's docs step follows once #296 is merged. The #296 page still lists --no-native without --host as network mode, which this PR changes.

Screenshots / Examples

Tested on Linux in Chromium and Firefox, on Windows 11 in Edge and a WebView2 window like the native app's. "Foreign host" is a test domain the browser resolves to 127.0.0.1, the state after a successful rebinding. The rebinding itself and browser protections against it were not tested, nor were the frozen Windows build and macOS. The new default address was checked on Linux with curl.

before after
--no-native without --host: address, foreign host all interfaces, works 127.0.0.1, 400
Foreign host, --no-native --host 127.0.0.1: list, download, delete a transcript all work 400
Foreign host, native window: list, delete, Models page all work 400
Foreign host, Edge on Windows: page and socket.io work 400
Page from another origin reads /archive blocked by the browser blocked
Page from another origin, socket.io handshake allowed 400
Normal use via 127.0.0.1, localhost, the native window and WebView2 works works
Proxy on the same machine that forwards its public host name (simulated with curl) works 400, works with --allow-any-host
nginx in default config on the same machine (simulated with curl) works page loads, socket.io 400, works with --allow-any-host
Any other address (tested with --host 127.0.0.2): foreign host and origin allowed allowed, unchanged

Verification Steps

  1. ATRAIN_USER_DIR=$(mktemp -d) uv run aTrain start --no-native --port 8090 --no-show, then ss -ltn shows it only on 127.0.0.1:8090.
  2. curl -s -o /dev/null -w "%{http_code}\n" -H "Host: rebound.example:8090" http://127.0.0.1:8090/ prints 400, without the header 200.
  3. curl -s -o /dev/null -w "%{http_code}\n" -H "Origin: https://example.com" "http://127.0.0.1:8090/_nicegui_ws/socket.io/?EIO=4&transport=polling" prints 400.
  4. Restart with --allow-any-host or with --host 127.0.0.2: step 2 prints 200.
  5. http://127.0.0.1:8090 in a browser and uv run aTrain start (native window) work as before.
  6. uv run --no-sync pytest tests/unit/test_hosts.py tests/e2e_browser/test_host_check.py

cc @gerardo-navarro

A website whose domain resolves to 127.0.0.1 (DNS rebinding) could load
and drive aTrain while it listened on loopback. When aTrain binds to
127.0.0.1 or localhost, as the native window does by default, the server
now answers only to those two host names. socket.io then only accepts
pages from the server's own origin. --allow-any-host turns both checks
off for a reverse proxy on the same machine. Servers bound to other
addresses are unchanged.

Refs aTrainTranscription#300
@JuergenFleiss

Copy link
Copy Markdown
Collaborator

Thanks @BW-Projects for taking this on.

I looked it through, and this generally looks good to me.

One point: testing confirmed that --no-native without --host still accepts foreign hostnames and Socket.IO connections from foreign origins. We should at least document this limitation with a clear warning. Preferably, we should extend the protections to this mode by default, with an explicit opt-out for network or reverse-proxy deployments.

@JuergenFleiss JuergenFleiss left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my previous comment

aTrain start --no-native without --host bound to all interfaces, where the
host and origin check did not apply. It now binds to 127.0.0.1 like the
native window, so the check applies there by default. Serving on the
network takes an explicit --host such as 0.0.0.0, which keeps the check
off as before.

Refs aTrainTranscription#300
@BW-Projects

Copy link
Copy Markdown
Contributor Author

@JuergenFleiss Thanks, I went with your preferred way:

  • --no-native binds to 127.0.0.1 by default, so the check applies there too.
  • Network mode needs an explicit --host like 0.0.0.0 and stays unchecked, as before.
  • --allow-any-host remains the opt-out for a reverse proxy on the same machine.
  • Existing --no-native setups on the network need --host 0.0.0.0 after the update. The Linux service guide now sets it and the CHANGELOG lists the new default.

Tested on Linux.

@JuergenFleiss JuergenFleiss left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, lgtm

@JuergenFleiss
JuergenFleiss merged commit 12de576 into aTrainTranscription:develop Nov 9, 2026
11 checks passed
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.

3 participants