fix(wiki): 关闭 WikiRelationController & WikiEntityController 的 IDOR 漏洞#439
Conversation
mateaix#438) WikiRelationController and WikiEntityController were missing the two-layer workspace authorization that WikiController already applies (@RequireWorkspaceRole + verifyKBWorkspace). Any authenticated user could read or trigger jobs on an arbitrary KB by guessing/traversing kbId. - Add @RequireWorkspaceRole("viewer") to read endpoints and "member" to write endpoints (enrich/repair/extract), matching WikiController's RBAC. - Add verifyKBWorkspace() to every kbId-bearing method; add verifyRawWorkspace()/verifyChunkWorkspace() that resolve the owning KB for the rawId/chunkId-only endpoints before the same check. - 18 unit tests cover: cross-workspace rejection (403), unknown resource (404), same-workspace allow, null-header default fallback, and null-workspaceId legacy KB tolerance. Regression-safe: the frontend injects X-Workspace-Id on every request and kbIds always originate from the caller's own workspace, so legitimate traffic is unaffected. Agent RAG retrieval is untouched — it calls HybridRetriever directly, not the HTTP layer. Closes mateaix#438
self-review of mateaix#439 found that getJobs checks the path kbId against the caller's workspace, but the optional ?rawId query param is independent and could point at another KB's job. A caller who knows any valid kbId in their own workspace could pass the kbId guard and then query an arbitrary rawId's processing-job status. Add a kbId ownership filter on the job returned by findLatestByRawId: if its kbId does not match the path kbId, return an empty list. Adds two tests covering the same-KB allow and cross-KB reject paths.
|
感谢这个 IDOR 修复,整体很扎实 👍 两层校验( 不过有同一类残留 IDOR 还没堵上,正好是你已经为
利用方式:认证用户传一个自己合法拥有的 建议照搬 补上这处后我们就合并 🙏 |
Review on mateaix#439 found that pageCitations checks the path kbId against the caller's workspace, but the {pageId} path variable is independent — a caller with a valid kbId could read citations (raw_title + ~200 char chunk snippet) for a page belonging to another KB. Same pattern as the getJobs(rawId) cross-KB filter already in this PR: resolve the page via pageService.getById, assert kbId.equals( page.getKbId()), return empty on mismatch. Adds 3 tests covering same-KB allow, cross-KB reject, and unknown pageId.
|
感谢抓住这个残留!确认属实,已照搬 // pageCitations — 查询前断言 pageId 属于路径 kbId
WikiPageEntity page = pageService.getById(pageId);
if (page == null || !kbId.equals(page.getKbId())) {
return List.of();
}
return citationMapper.listWithRawByPageId(pageId);补了 3 个测试覆盖:同 KB 放行 / 跨 KB 返回空 / 未知 pageId 返回空。 现在 |
|
感谢补上这处 🙏 复核了全部端点的独立 id 参数( |
背景
Closes #438
WikiRelationController与WikiEntityController缺失WikiController已有的两层工作区鉴权(@RequireWorkspaceRole+verifyKBWorkspace),任何已登录用户通过遍历kbId即可越权读取 / 触发其他工作区知识库的处理任务。详情见 #438。改动
WikiRelationController(10 个接口)GET /kb/{kbId}/pages/{slug}/relatedGET /kb/{kbId}/pages/{slugA}/relation/{slugB}GET /raw/{rawId}/pagesGET /chunks/{chunkId}/pagesGET /kb/{kbId}/pages/{pageId}/citationsGET /kb/{kbId}/jobsGET /kb/{kbId}/statsPOST /kb/{kbId}/search-previewPOST /kb/{kbId}/pages/{slug}/enrichPOST /kb/{kbId}/pages/{slug}/repairWikiEntityController(4 个接口)GET /kb/{kbId}/entitiesGET /kb/{kbId}/entity-graphGET /kb/{kbId}/entities/{entityId}/graphPOST /kb/{kbId}/entities/extract新增三个 private 校验方法,完全对齐
WikiController的既有范式:verifyKBWorkspace(kbId, workspaceId)— 与WikiController同名方法一致。verifyRawWorkspace(rawId, workspaceId)— 先解析 raw → kbId,再复用上面的 KB 校验(raw 不直接持有 workspaceId)。verifyChunkWorkspace(chunkId, workspaceId)— 同理,chunk → kbId → 校验。测试
新增 18 个单元测试(复用 #415 的 IDOR 测试范式):
WikiRelationControllerIdorTest(12 个)WikiEntityControllerIdorTest(6 个)现有相关测试无回归:
WikiTransformationControllerTest(2) +WikiHotCacheControllerTest(6) +WikiToolPermissionTest(11) +WikiPageTypePermissionServiceTest(14) 全绿。回归安全
http拦截器对每个请求注入X-Workspace-Id,用户操作的 kbId 均来自自己工作区的知识库列表,workspaceId必然匹配。HybridRetriever.search()(WikiSkillWrapperToolFactory/WikiTool/WikiContextService),不经 HTTP。