+ "details": "### Summary\nA cross-tenant authorization bypass in the knowledge base copy endpoint allows any authenticated user to clone (duplicate) another tenant’s knowledge base into their own tenant by knowing/guessing the source knowledge base ID. This enables bulk data exfiltration (document/FAQ content) across tenants, making the impact critical.\n\n### Details\n\nThe `POST /api/v1/knowledge-bases/copy` endpoint enqueues an asynchronous KB clone task using the caller-supplied `source_id` without verifying ownership (see `internal/handler/knowledgebase.go`).\n```go\n// Create KB clone payload\npayload := types.KBClonePayload{\n TenantID: tenantID.(uint64),\n TaskID: taskID,\n SourceID: req.SourceID, // from attacker's input\n TargetID: req.TargetID,\n}\n\npayloadBytes, err := json.Marshal(payload)\nif err != nil {\n logger.Errorf(ctx, \"Failed to marshal KB clone payload: %v\", err)\n c.Error(errors.NewInternalServerError(\"Failed to create task\"))\n return\n}\n\n// Enqueue KB clone task to Asynq\ntask := asynq.NewTask(types.TypeKBClone, payloadBytes,\nasynq.TaskID(taskID), asynq.Queue(\"default\"), asynq.MaxRetry(3)) // enqueue task\ninfo, err := h.asynqClient.Enqueue(task)\nif err != nil {\n logger.Errorf(ctx, \"Failed to enqueue KB clone task: %v\", err)\n c.Error(errors.NewInternalServerError(\"Failed to enqueue task\"))\n return\n}\n```\n\nThen, the asynq task handler (`ProcessKBClone`) invokes the `CopyKnowledgeBase` service method to perform the clone operation (see `internal/application/service/knowledge.go`):\n\n```go\n// Get source and target knowledge bases\nsrcKB, dstKB, err := s.kbService.CopyKnowledgeBase(ctx, payload.SourceID, payload.TargetID)\nif err != nil {\n logger.Errorf(ctx, \"Failed to copy knowledge base: %v\", err)\n handleError(progress, err, \"Failed to copy knowledge base configuration\")\n return err\n}\n```\n\nAfter that, the `CopyKnowledgeBase` method calls the repository method to load the source knowledge base (see `internal/application/service/knowledgebase.go`):\n\n```go\nfunc (s *knowledgeBaseService) CopyKnowledgeBase(ctx context.Context,\n\tsrcKB string, dstKB string,\n) (*types.KnowledgeBase, *types.KnowledgeBase, error) {\n\tsourceKB, err := s.repo.GetKnowledgeBaseByID(ctx, srcKB)\n\tif err != nil {\n\t\tlogger.Errorf(ctx, \"Get source knowledge base failed: %v\", err)\n\t\treturn nil, nil, err\n\t}\n\tsourceKB.EnsureDefaults()\n\ttenantID := ctx.Value(types.TenantIDContextKey).(uint64)\n\tvar targetKB *types.KnowledgeBase\n\tif dstKB != \"\" {\n\t\ttargetKB, err = s.repo.GetKnowledgeBaseByID(ctx, dstKB)\n // ...\n }\n // ...\n}\n```\n\n\n> Note: until now, the tenant ID is correctly set in context to the attacker’s tenant (from the payload), which can be used to prevent cross-tenant access.\n\nHowever, the repository method `GetKnowledgeBaseByID` loads knowledge bases by `id` only, allowing cross-tenant reads (see `internal/application/repository/knowledgebase.go`).\n\n```go\nfunc (r *knowledgeBaseRepository) GetKnowledgeBaseByID(ctx context.Context, id string) (*types.KnowledgeBase, error) {\n\tvar kb types.KnowledgeBase\n\tif err := r.db.WithContext(ctx).Where(\"id = ?\", id).First(&kb).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, ErrKnowledgeBaseNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &kb, nil\n}\n```\n\nThe data access layer fails to enforce tenant isolation because `GetKnowledgeBaseByID` only filters by ID and ignores the `tenant_id` present in the context. A secure implementation should enforce a tenant-scoped lookup (e.g., `WHERE id = ? AND tenant_id = ?`) or use a tenant-aware repository API to prevent cross-tenant access.\n\nService shallow-copies the KB configuration by calling `GetKnowledgeBaseByID(ctx, srcKB)` for the source KB, then creates a new KB under the attacker’s tenant while copying fields from the victim KB (`internal/application/service/knowledgebase.go`):\n\n```go\nsourceKB, err := s.repo.GetKnowledgeBaseByID(ctx, srcKB) // not tenant-scoped\n...\ntargetKB = &types.KnowledgeBase{\n ID: uuid.New().String(),\n Name: sourceKB.Name,\n Type: sourceKB.Type,\n Description: sourceKB.Description,\n TenantID: tenantID,\n ChunkingConfig: sourceKB.ChunkingConfig,\n ImageProcessingConfig: sourceKB.ImageProcessingConfig,\n EmbeddingModelID: sourceKB.EmbeddingModelID,\n SummaryModelID: sourceKB.SummaryModelID,\n VLMConfig: sourceKB.VLMConfig,\n StorageConfig: sourceKB.StorageConfig,\n FAQConfig: faqConfig,\n}\ntargetKB.EnsureDefaults()\n if err := s.repo.CreateKnowledgeBase(ctx, targetKB); err != nil {\n return nil, nil, err\n }\n}\n```\n\n### PoC\n\nPrecondition: Attacker is authenticated in Tenant A and can obtain (or guess) a victim's knowledge base UUID belonging to Tenant B.\n\n1) Authenticate as Tenant A and obtain a bearer token or API key.\n\n2) Start a cross-tenant clone using the victim’s knowledge base ID as `source_id`:\n\n```bash\ncurl -X POST http://localhost:8088/api/v1/knowledge-bases/copy \\\n -H \"Authorization: Bearer <ATTACKER_TOKEN>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"source_id\":\"<VICTIM_KB_UUID>\",\"target_id\":\"\"}'\n```\n\n3) Observe that the task is accepted:\n- HTTP `200 OK`\n- Response contains a `task_id` and a message like `\"Knowledge base copy task started\"`.\n\n4) After the async task completes, a new knowledge base appears under Tenant A containing copied content/config from Tenant B.\n\n> Note: the copy can succeed even when models referenced by the source KB do not exist in the attacker tenant, indicating the workflow does not validate model ownership during copy.\n\nPoC Video:\n\nhttps://github.com/user-attachments/assets/8313fa44-5d5d-43f4-8ebd-f465c5a9d56e\n\n### Impact\n\nThis is a Broken Access Control (BOLA/IDOR) vulnerability enabling cross-tenant data exfiltration:\n\n- Any authenticated user can trigger a clone of a victim tenant’s knowledge base into their own tenant.\n- Results in bulk disclosure/duplication of knowledge base contents (documents/FAQ entries/chunks), plus associated configuration.",
0 commit comments