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:
+27
-14
@@ -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}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user