fix(security): BL-07 WS 403 澄清与 code-review 跟进加固

外网无 token WebSocket 的 HTTP 403 为应用层拒绝而非反代故障;边端 agent
在中心 401 时停止心跳重试;install 锁定路由级 404 且归档失败 fail-closed。
同步安全规范与 BL-06 一致,门控 7/7 PASS。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-07 12:12:49 +08:00
parent eab35a35be
commit 72d82d737b
11 changed files with 324 additions and 17 deletions
+27 -14
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from datetime import datetime, timezone
from urllib.parse import quote_plus, unquote_plus, urlparse
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
@@ -24,8 +24,6 @@ from server.utils.text_io import read_utf8_text, write_utf8_lf
logger = logging.getLogger("nexus.install")
router = APIRouter(prefix="/api/install", tags=["install"])
# ── Paths ──
ROOT_DIR = Path(__file__).resolve().parent.parent.parent
@@ -53,6 +51,19 @@ def _is_installed() -> bool:
return _is_locked()
def _raise_not_found_if_locked() -> None:
"""Hide install API fingerprint after lock — same response as unknown route."""
if _is_locked():
raise HTTPException(status_code=404, detail="Not found")
router = APIRouter(
prefix="/api/install",
tags=["install"],
dependencies=[Depends(_raise_not_found_if_locked)],
)
# ── Pydantic Models ──
class InitDbRequest(BaseModel):
@@ -903,7 +914,8 @@ def _archive_install_wizard_html() -> None:
try:
INSTALL_HTML.unlink()
except OSError as e:
logger.warning("Failed to remove install.html after archive: %s", e)
logger.error("Failed to remove install.html after archive: %s", e)
raise RuntimeError("Failed to remove install wizard HTML after lock") from e
return
if not INSTALL_HTML.is_file():
return
@@ -911,18 +923,19 @@ def _archive_install_wizard_html() -> None:
INSTALL_HTML.rename(INSTALL_HTML_BAK)
logger.info("Archived install wizard: %s%s", INSTALL_HTML, INSTALL_HTML_BAK)
except OSError as e:
logger.warning("Failed to archive install.html: %s", e)
logger.error("Failed to archive install.html: %s", e)
raise RuntimeError("Failed to archive install wizard HTML") from e
def _finalize_install_lock() -> None:
"""Write install lock marker and remove wizard state file."""
_sync_env_to_persist_volume()
_archive_install_wizard_html()
write_utf8_lf(
INSTALL_LOCK,
f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n",
)
_sync_lock_to_persist_volume()
_archive_install_wizard_html()
if STATE_FILE.exists():
try:
STATE_FILE.unlink()
@@ -948,12 +961,6 @@ def _verify_install_token(provided: str) -> None:
# ── Endpoints ──
def _raise_not_found_if_locked() -> None:
"""Hide install API fingerprint after lock — same response as unknown route."""
if _is_locked():
raise HTTPException(status_code=404, detail="Not found")
@router.get("/status")
async def install_status():
"""Check whether Nexus is already installed."""
@@ -1399,7 +1406,10 @@ async def create_admin(req: CreateAdminRequest):
write_utf8_lf(STATE_FILE, json.dumps(state, ensure_ascii=False) + "\n")
# Atomically lock after admin creation (closes unauthenticated install API window)
_finalize_install_lock()
try:
_finalize_install_lock()
except RuntimeError as e:
raise HTTPException(status_code=500, detail=str(e)) from e
return {"success": True, "locked": True}
@@ -1439,7 +1449,10 @@ async def lock_install():
detail="数据库暂时不可用,无法验证管理员账户,请稍后重试。",
) from e
_finalize_install_lock()
try:
_finalize_install_lock()
except RuntimeError as e:
raise HTTPException(status_code=500, detail=str(e)) from e
return {"success": True}