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
+14 -6
View File
@@ -14,9 +14,10 @@ import platform as _platform
from pathlib import Path
from fastapi import FastAPI, HTTPException, Body
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from fastapi.responses import StreamingResponse, FileResponse
from .nexus_config import settings, VERSION, DEFAULT_CHAT_MODEL
from .nexus_config import settings, VERSION, DEFAULT_CHAT_MODEL, ALLOWED_HOSTS, ALLOWED_ORIGINS
from .chat import generate_chat_response, stream_chat_response, _synapse_trace
from . import chat as _chat
from .ollama_manager import initialize_ollama, initialize_ollama_async, get_ollama_manager
@@ -179,13 +180,20 @@ app = FastAPI(title="Synapse Backend", version=VERSION)
# Alias for startup scripts
sio_app = app
# --- CORS ---
# allow_credentials=True is incompatible with allow_origins=["*"] — browsers
# reject such responses. Since this is a local-only service with no cookies/auth,
# wildcard origins without credentials is correct.
# --- Local-access guard ---
# These APIs are unauthenticated and meant for this machine only. Two layers:
# 1. TrustedHostMiddleware rejects a foreign Host header, which is what defeats
# DNS-rebinding — the browser treats a rebound attacker domain as
# same-origin, so CORS alone can't stop it.
# 2. CORS is scoped to known local origins (not "*"), so a malicious page can't
# read responses cross-origin from the victim's browser.
# NEXUS_ALLOWED_HOSTS=* / a custom NEXUS_ALLOWED_ORIGINS opts into LAN exposure,
# and should only be used once real authentication is in front of these apps.
if "*" not in ALLOWED_HOSTS:
app.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=ALLOWED_ORIGINS,
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],