Skip to content

fix(wiki): 关闭 WikiRelationController & WikiEntityController 的 IDOR 漏洞#439

Merged
mateaix merged 3 commits into
mateaix:devfrom
ncw1992120:fix/wiki-relation-controller-idor
Jun 28, 2026
Merged

fix(wiki): 关闭 WikiRelationController & WikiEntityController 的 IDOR 漏洞#439
mateaix merged 3 commits into
mateaix:devfrom
ncw1992120:fix/wiki-relation-controller-idor

Conversation

@ncw1992120

Copy link
Copy Markdown
Contributor

背景

Closes #438

WikiRelationControllerWikiEntityController 缺失 WikiController 已有的两层工作区鉴权(@RequireWorkspaceRole + verifyKBWorkspace),任何已登录用户通过遍历 kbId 即可越权读取 / 触发其他工作区知识库的处理任务。详情见 #438

改动

WikiRelationController(10 个接口)

接口 角色 新增校验
GET /kb/{kbId}/pages/{slug}/related viewer verifyKBWorkspace
GET /kb/{kbId}/pages/{slugA}/relation/{slugB} viewer verifyKBWorkspace
GET /raw/{rawId}/pages viewer verifyRawWorkspace(解析 KB 后校验)
GET /chunks/{chunkId}/pages viewer verifyChunkWorkspace(解析 KB 后校验)
GET /kb/{kbId}/pages/{pageId}/citations viewer verifyKBWorkspace
GET /kb/{kbId}/jobs viewer verifyKBWorkspace
GET /kb/{kbId}/stats viewer verifyKBWorkspace
POST /kb/{kbId}/search-preview viewer verifyKBWorkspace
POST /kb/{kbId}/pages/{slug}/enrich member verifyKBWorkspace
POST /kb/{kbId}/pages/{slug}/repair member verifyKBWorkspace

WikiEntityController(4 个接口)

接口 角色 新增校验
GET /kb/{kbId}/entities viewer verifyKBWorkspace
GET /kb/{kbId}/entity-graph viewer verifyKBWorkspace
GET /kb/{kbId}/entities/{entityId}/graph viewer verifyKBWorkspace
POST /kb/{kbId}/entities/extract member verifyKBWorkspace

新增三个 private 校验方法,完全对齐 WikiController 的既有范式:

  • verifyKBWorkspace(kbId, workspaceId) — 与 WikiController 同名方法一致。
  • verifyRawWorkspace(rawId, workspaceId) — 先解析 raw → kbId,再复用上面的 KB 校验(raw 不直接持有 workspaceId)。
  • verifyChunkWorkspace(chunkId, workspaceId) — 同理,chunk → kbId → 校验。

测试

新增 18 个单元测试(复用 #415 的 IDOR 测试范式):

WikiRelationControllerIdorTest(12 个)

  • ✅ search-preview / stats / enrich / repair 跨工作区 → 403
  • ✅ rawId / chunkId 跨工作区(解析 KB 后) → 403
  • ✅ 未知 kbId / rawId / chunkId → 404
  • ✅ 同工作区放行(不抛异常)
  • ✅ 无 X-Workspace-Id header → fallback 默认 ws=1
  • ✅ KB.workspaceId=null(旧数据)→ 放行

WikiEntityControllerIdorTest(6 个)

  • ✅ listEntities / entityGraph / egoGraph / extract 跨工作区 → 403
  • ✅ 未知 kbId → 404
  • ✅ 同工作区放行
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

现有相关测试无回归:WikiTransformationControllerTest(2) + WikiHotCacheControllerTest(6) + WikiToolPermissionTest(11) + WikiPageTypePermissionServiceTest(14) 全绿。

回归安全

  • 前端零影响http 拦截器对每个请求注入 X-Workspace-Id,用户操作的 kbId 均来自自己工作区的知识库列表,workspaceId 必然匹配。
  • Agent 检索不受影响:Agent 调用知识库走 Java 方法 HybridRetriever.search()WikiSkillWrapperToolFactory / WikiTool / WikiContextService),不经 HTTP。
  • 唯一边角:第三方直连脚本若访问非默认工作区(id≠1)知识库且未带 header,会从"可访问"变为 403——但这正是漏洞本身。

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.
@mateaix

mateaix commented Jun 28, 2026

Copy link
Copy Markdown
Owner

感谢这个 IDOR 修复,整体很扎实 👍 两层校验(@RequireWorkspaceRole + verifyKBWorkspace)完整覆盖了 14 个端点,和 WikiController 的既有模式逐字一致;verifyRawWorkspace/verifyChunkWorkspace 的「先解析再校验」也正确,getJobs(rawId) 的跨 KB 过滤是个很好的自查。测试也很到位。

不过有同一类残留 IDOR 还没堵上,正好是你已经为 getJobs(rawId) 修过的那一类:

WikiRelationController.pageCitations —— handler 先用路径里的 kbId 过了 verifyKBWorkspace,但随后直接 citationMapper.listWithRawByPageId(pageId),而 pageId 是独立的、完全由调用方提供的 @PathVariable从未与 kbId 绑定校验。对应的 WikiPageCitationMapper.listWithRawByPageId 里 SQL 只有 WHERE c.page_id = #{pageId},没有 kbId 谓词。

利用方式:认证用户传一个自己合法拥有的 kbId(通过 verifyKBWorkspace)+ 一个属于别的 workspace 的 pageId,就能读到那个页面的 citations —— 包含 raw_title 和 ~200 字的 chunk snippet。这比原始漏洞更敏感(泄露的是实际文档内容,而非任务状态/计数)。

建议照搬 getJobs 的修法:查询前先 pageService.getById(pageId),断言 kbId.equals(page.getKbId())(不匹配返回空 / 404)。WikiPageService.getById(Long)WikiPageEntity.getKbId() 都现成,改动很小、和 PR 其余部分一致。

补上这处后我们就合并 🙏

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.
@ncw1992120

Copy link
Copy Markdown
Contributor Author

感谢抓住这个残留!确认属实,已照搬 getJobs(rawId) 的修法补上(b0731002):

// 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 返回空。

Tests run: 23, Failures: 0, Errors: 0 (12→15 + entity 6 + getJobs 2)
BUILD SUCCESS

现在 WikiRelationController 全部 10 个端点的独立 ID 变量(kbId / rawId / chunkId / pageId)都已做归属校验,应该没有遗漏了 🙏

@mateaix

mateaix commented Jun 28, 2026

Copy link
Copy Markdown
Owner

感谢补上这处 🙏 pageCitations 的残留 IDOR 已经按建议堵好了:@RequireWorkspaceRole("viewer") + verifyKBWorkspace(kbId) 两层校验,再加 pageService.getById(pageId) 后断言 kbId.equals(page.getKbId()),不匹配/不存在返回空列表,和 getJobs(rawId) 的跨 KB 过滤同一套路;还补了 same-KB / cross-KB / unknown 三个单测。

复核了全部端点的独立 id 参数(rawIdverifyRawWorkspacechunkIdverifyChunkWorkspacepageId→ kbId 绑定、slug 都在 kbId 作用域内),没有遗留同类问题。已合并到 dev

@mateaix mateaix merged commit 2f46619 into mateaix:dev Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

安全|WikiRelationController 全部接口缺失工作区鉴权(IDOR)

2 participants