fix(nginx): defer CORS to gateway allowlist (#2861)

* fix(nginx): defer cors to gateway allowlist

Remove proxy-level wildcard CORS handling so browser origins are controlled by the Gateway allowlist and stay aligned with CSRF origin checks.

* docs: document gateway cors allowlist

Clarify that same-origin nginx access needs no CORS headers while split-origin or port-forwarded browser clients must opt in with GATEWAY_CORS_ORIGINS.

* docs(gateway): record cors source of truth

Document that Gateway CORSMiddleware and CSRFMiddleware share GATEWAY_CORS_ORIGINS as the split-origin source of truth.

* fix(gateway): align cors origin normalization

* docs: clarify gateway langgraph routing

* docs(gateway): update runtime routing note
This commit is contained in:
AochenShen99
2026-05-11 17:38:37 +08:00
committed by GitHub
parent 813d3c94ef
commit c3bc6c7cd5
14 changed files with 169 additions and 130 deletions
+42
View File
@@ -122,3 +122,45 @@ def test_health_still_works_when_docs_disabled():
resp = client.get("/health")
assert resp.status_code == 200
assert resp.json()["status"] == "healthy"
# ---------------------------------------------------------------------------
# Runtime CORS behavior
# ---------------------------------------------------------------------------
def _make_gateway_client(cors_origins: str) -> TestClient:
with patch.dict(os.environ, {"GATEWAY_CORS_ORIGINS": cors_origins}):
_reset_gateway_config()
from app.gateway.app import create_app
return TestClient(create_app())
def test_gateway_cors_allows_configured_origin():
"""GATEWAY_CORS_ORIGINS should control actual browser CORS responses."""
client = _make_gateway_client("https://app.example")
response = client.get("/health", headers={"Origin": "https://app.example"})
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "https://app.example"
assert response.headers["access-control-allow-credentials"] == "true"
def test_gateway_cors_rejects_unconfigured_origin():
client = _make_gateway_client("https://app.example")
response = client.get("/health", headers={"Origin": "https://evil.example"})
assert response.status_code == 200
assert "access-control-allow-origin" not in response.headers
def test_gateway_cors_normalizes_configured_default_port():
client = _make_gateway_client("https://app.example:443")
response = client.get("/health", headers={"Origin": "https://app.example"})
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "https://app.example"
@@ -53,6 +53,29 @@ def test_nginx_routes_official_langgraph_prefix_to_gateway_api():
assert "proxy_pass http://gateway" in content or "proxy_pass http://$gateway_upstream" in content
def test_nginx_defers_cors_to_gateway_allowlist():
for path in ("docker/nginx/nginx.local.conf", "docker/nginx/nginx.conf"):
content = _read(path)
assert "Access-Control-Allow-Origin" not in content
assert "Access-Control-Allow-Methods" not in content
assert "Access-Control-Allow-Headers" not in content
assert "Access-Control-Allow-Credentials" not in content
assert "proxy_hide_header 'Access-Control-Allow-" not in content
assert "if ($request_method = 'OPTIONS')" not in content
def test_gateway_cors_configuration_uses_gateway_allowlist():
gateway_config = _read("backend/app/gateway/config.py")
gateway_app = _read("backend/app/gateway/app.py")
csrf_middleware = _read("backend/app/gateway/csrf_middleware.py")
assert not re.search(r"(?<!GATEWAY_)[\"']CORS_ORIGINS[\"']", gateway_config)
assert "cors_origins" not in gateway_config
assert "get_configured_cors_origins" in gateway_app
assert "GATEWAY_CORS_ORIGINS" in csrf_middleware
def test_frontend_rewrites_langgraph_prefix_to_gateway():
next_config = _read("frontend/next.config.js")
api_client = _read("frontend/src/core/api/api-client.ts")