feat(security): bind services to loopback with Host + CORS allowlists

The Synapse backend and memory service bound 0.0.0.0 with wildcard CORS and no
auth, exposing the full unauthenticated admin/data API to the LAN. Default the
uvicorn bind to 127.0.0.1 (NEXUS_BIND_HOST override), scope CORS to known local
origins instead of "*", and add TrustedHostMiddleware to reject foreign Host
headers (which defeats DNS-rebinding, something same-origin CORS cannot stop).

NEXUS_ALLOWED_HOSTS / NEXUS_ALLOWED_ORIGINS allow opt-in LAN exposure, intended
to be paired with real authentication.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 09:40:12 -05:00
co-authored by Cursor
parent e8379786d3
commit fe12eb1821
4 changed files with 54 additions and 9 deletions
+7 -1
View File
@@ -112,6 +112,12 @@ class Service:
return self._argv() if callable(self._argv) else self._argv
# Bind loopback by default: the backend/memory REST APIs are unauthenticated, so
# binding 0.0.0.0 handed the full admin+data plane to any host on the LAN. Set
# NEXUS_BIND_HOST=0.0.0.0 to opt into LAN exposure once real auth is in place.
BIND_HOST = os.environ.get("NEXUS_BIND_HOST", "127.0.0.1")
def _uvicorn(app: str, port: int):
# No --reload. It is a dev-loop flag: uvicorn's reloader runs a supervisor
# that spawns the real server as a CHILD, so every service became two
@@ -120,7 +126,7 @@ def _uvicorn(app: str, port: int):
# terminals for what should have been a silent start. launch_nexus.sh still
# passes --reload for the Linux dev loop, where a visible console is the
# point; this launcher is the one users run.
return [str(PYTHON), "-m", "uvicorn", app, "--host", "0.0.0.0",
return [str(PYTHON), "-m", "uvicorn", app, "--host", BIND_HOST,
"--port", str(port)]