fix: address audit findings — session_id validation, streaming reset, state isolation
- Replace truncated 12-char UUID with full 32-char UUID (128-bit entropy) - Add validate_session_id() regex check to prevent path traversal - Add _check_session_id() guard on all 6 API endpoints - Change _step_counter from module global to contextvars.ContextVar - Filter None values from node_state before merging into agent_state - Log save_session failures instead of silently swallowing them - Add finishStreaming() in catch/finally blocks to prevent UI lockup - Fix broken multiline docstring in chat() endpoint
This commit is contained in:
@@ -59,7 +59,7 @@ class TestSessionCRUD:
|
||||
resp = client.post("/api/sessions")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data["session_id"]) == 12
|
||||
assert len(data["session_id"]) == 32
|
||||
assert "session_name" in data
|
||||
assert "created_at" in data
|
||||
|
||||
@@ -78,8 +78,11 @@ class TestSessionCRUD:
|
||||
assert resp.json()["session_id"] == created["session_id"]
|
||||
assert "agent_state" in resp.json()
|
||||
|
||||
def test_get_session_invalid_id(self, client, temp_sessions):
|
||||
assert client.get("/api/sessions/nonexistent").status_code == 400
|
||||
|
||||
def test_get_session_not_found(self, client, temp_sessions):
|
||||
assert client.get("/api/sessions/nonexistent").status_code == 404
|
||||
assert client.get("/api/sessions/aabbccddeeff0011223344").status_code == 404
|
||||
|
||||
def test_delete_session(self, client, temp_sessions):
|
||||
sid = client.post("/api/sessions").json()["session_id"]
|
||||
@@ -89,7 +92,7 @@ class TestSessionCRUD:
|
||||
assert client.get(f"/api/sessions/{sid}").status_code == 404
|
||||
|
||||
def test_delete_nonexistent(self, client, temp_sessions):
|
||||
assert client.delete("/api/sessions/ghost_id").status_code == 404
|
||||
assert client.delete("/api/sessions/aabbccddeeff0011223344").status_code == 404
|
||||
|
||||
def test_full_crud_lifecycle(self, client, temp_sessions):
|
||||
sid = client.post("/api/sessions").json()["session_id"]
|
||||
@@ -116,7 +119,7 @@ class TestFileUpload:
|
||||
|
||||
def test_upload_with_session_id_in_query(self, client, temp_sessions):
|
||||
resp = client.post(
|
||||
"/api/upload?session_id=abc123",
|
||||
"/api/upload?session_id=aabbccddeeff0011223344",
|
||||
files={"file": ("data.csv", io.BytesIO(b"a,b,c"), "text/csv")},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -146,7 +149,7 @@ class TestFileUpload:
|
||||
|
||||
class TestDownload:
|
||||
def test_download_missing_session_returns_404(self, client, temp_sessions):
|
||||
assert client.get("/api/sessions/missing/download/latest").status_code == 404
|
||||
assert client.get("/api/sessions/aabbccddeeff0011223344/download/latest").status_code == 404
|
||||
|
||||
def test_download_no_jrxml_returns_404(self, client, temp_sessions):
|
||||
sid = client.post("/api/sessions").json()["session_id"]
|
||||
@@ -212,7 +215,7 @@ class TestChatSSE:
|
||||
def test_auto_creates_session_on_chat(self, client, temp_sessions):
|
||||
with client.stream(
|
||||
"POST",
|
||||
"/api/sessions/auto_new_session/chat",
|
||||
"/api/sessions/aabbccddeeff0011223344/chat",
|
||||
json={"text": "生成报表", "file_ids": []},
|
||||
) as resp:
|
||||
assert resp.status_code == 200
|
||||
@@ -231,16 +234,17 @@ class TestChatSSE:
|
||||
# ── 边界 & 安全测试 ────────────────────────────────────────────
|
||||
|
||||
class TestBoundaries:
|
||||
def test_session_id_path_traversal_returns_404(self, client, temp_sessions):
|
||||
assert client.get("/api/sessions/../etc/passwd").status_code == 404
|
||||
def test_session_id_invalid_format_returns_400(self, client, temp_sessions):
|
||||
"""非 hex 字符的 session_id 应被拒绝。"""
|
||||
assert client.get("/api/sessions/not_valid_hex_id").status_code == 400
|
||||
|
||||
def test_upload_with_path_traversal_session_id(self, client, temp_sessions):
|
||||
"""路径穿越 session_id 仍正常处理(目录隔离在 UPLOADS_DIR 内)。"""
|
||||
"""路径穿越 session_id 被拒绝。"""
|
||||
resp = client.post(
|
||||
"/api/upload?session_id=../malicious",
|
||||
files={"file": ("t.txt", io.BytesIO(b"x"), "text/plain")},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_invalid_json_body_rejected(self, client, temp_sessions):
|
||||
sid = client.post("/api/sessions").json()["session_id"]
|
||||
|
||||
@@ -41,7 +41,7 @@ def temp_sessions_dir(monkeypatch):
|
||||
class TestCreateSession:
|
||||
def test_creates_with_defaults(self, temp_sessions_dir):
|
||||
s = create_session()
|
||||
assert len(s["session_id"]) == 12
|
||||
assert len(s["session_id"]) == 32
|
||||
assert "新建报表" in s["session_name"]
|
||||
assert s["created_at"]
|
||||
assert s["updated_at"]
|
||||
@@ -139,7 +139,7 @@ class TestSaveSession:
|
||||
assert load_session(created["session_id"])["session_name"] == "原名"
|
||||
|
||||
def test_fills_missing_created_at(self, temp_sessions_dir):
|
||||
sid = "test_no_created"
|
||||
sid = "aaaabbbbccccddddeeeeffff"
|
||||
fp = temp_sessions_dir / f"{sid}.json"
|
||||
fp.write_text(
|
||||
json.dumps({"session_id": sid, "session_name": "旧数据"}), "utf-8"
|
||||
|
||||
Reference in New Issue
Block a user