Files
Agent-System/internal/server/chat_session_ownership_pg_test.go
weichenandCursor ba1fc71f16 test(server): 跨用户会话挂靠负例测试(PG + 内存双实现)
覆盖 chat / chat/stream / chat/sessions 三条路径;受害者续用不受影响。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 03:39:08 +00:00

80 lines
2.9 KiB
Go

package server_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestChatSessionOwnership_CrossUserHijackDenied_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
handler, _ := setupPGGovernanceServer(t, pool)
victimToken := loginTestUser(t, handler, "zhanggong", "123456")
attackerToken := loginTestUser(t, handler, "lizhuguan", "123456")
sessionID := "sess-hijack-pg-" + time.Now().Format("150405.000")
victimBody, _ := json.Marshal(map[string]any{"session_id": sessionID, "message": "机密立项讨论"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat", bytes.NewReader(victimBody))
req.Header.Set("Content-Type", "application/json")
withBearerAuth(req, victimToken)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("victim chat: %d %s", rec.Code, rec.Body.String())
}
attackerBody, _ := json.Marshal(map[string]any{"session_id": sessionID, "message": "尝试接管"})
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/chat", bytes.NewReader(attackerBody))
req2.Header.Set("Content-Type", "application/json")
withBearerAuth(req2, attackerToken)
rec2 := httptest.NewRecorder()
handler.ServeHTTP(rec2, req2)
if rec2.Code != http.StatusForbidden {
t.Fatalf("attacker hijack expected 403, got %d: %s", rec2.Code, rec2.Body.String())
}
if !strings.Contains(rec2.Body.String(), "SESSION_FORBIDDEN") {
t.Fatalf("expected SESSION_FORBIDDEN, got %s", rec2.Body.String())
}
req3 := httptest.NewRequest(http.MethodPost, "/api/v1/chat/stream", bytes.NewReader(attackerBody))
req3.Header.Set("Content-Type", "application/json")
withBearerAuth(req3, attackerToken)
rec3 := httptest.NewRecorder()
handler.ServeHTTP(rec3, req3)
if rec3.Code != http.StatusForbidden {
t.Fatalf("attacker stream hijack expected 403, got %d", rec3.Code)
}
if strings.Contains(rec3.Header().Get("Content-Type"), "text/event-stream") {
t.Fatal("must reject before SSE headers are set")
}
upsertBody, _ := json.Marshal(map[string]any{"session_id": sessionID, "user_id": 999999, "title": "hijack via upsert"})
req4 := httptest.NewRequest(http.MethodPost, "/api/v1/chat/sessions", bytes.NewReader(upsertBody))
req4.Header.Set("Content-Type", "application/json")
withBearerAuth(req4, attackerToken)
rec4 := httptest.NewRecorder()
handler.ServeHTTP(rec4, req4)
if rec4.Code != http.StatusForbidden {
t.Fatalf("attacker upsert hijack expected 403, got %d: %s", rec4.Code, rec4.Body.String())
}
req5 := httptest.NewRequest(http.MethodPost, "/api/v1/chat", bytes.NewReader(victimBody))
req5.Header.Set("Content-Type", "application/json")
withBearerAuth(req5, victimToken)
rec5 := httptest.NewRecorder()
handler.ServeHTTP(rec5, req5)
if rec5.Code != http.StatusOK {
t.Fatalf("victim continued access broken: %d %s", rec5.Code, rec5.Body.String())
}
}