权限链路复核:gfc 到底是怎么拿到 openKylin 组织 admin 的
复核时间:2026-11-09 | 对象:openKylin 论坛 #173855、「权限攫取」一节
新增证据源:Gitee PR API、公开 Jenkins 构建日志(jenkins.openkylin.top/job/sig-changed/*/consoleText)、openkylin/docs 官方流程文档、openkylin/community 仓库配置。
0. 一句话结论
不是 Gitee 平台漏洞,不是账号被盗,不是 bot 被入侵,也不是「轻量级 PR 自动合并」。
是 openKylin 自己把「sig/<SIG>/sig.yaml 里的 maintainer 名单」当成了权限授予的唯一凭据,并把「PR 被标记为 approved/check_pass」当成了唯一授权闸门。sig-bot.py 在 PR 合并之前就直接调 Gitee API 给仓库加 admin,permissions/gitee.json 只是事后的结果快照。官方流程文档写明的 CLA、技术委员会审核、例会投票三道人工程序,一道都没有被实现成技术门禁。
更致命的一点:第一批 33 个 admin 里就包含 openkylin/community 治理仓库自己——拿到治理仓库的写权限后,就能自己合并自己的 PR,形成自举闭环。
1. 真实链路(与原帖有两处实质出入)
① 任意 Gitee 用户(非组织成员、未签 CLA)
改 sig/<SIG>/sig.yaml 的 maintainers,+1 行,提交 PR
↓
② PR 事件 open → Jenkins「sig-changed」→ python3 sig-bot.py -r gitee
= dry run,输出「将 add/remove 谁到哪个仓库」的清单,(dry run skip) 不实做
↓
③ PR 状态变为 approved / check_pass ← ★ 唯一的授权闸门
↓
④ Jenkins「sig-changed」再次运行 sig-bot.py
实做:逐仓调 Gitee API,add/remove 协作者,直接给 admin
↓ 同一分钟
⑤ cibot 提交 sig/sig.yaml + permissions/gitee.json(结果快照,不是输入)
↓
⑥ PR 合并 → 下一次全量重算,把之前所有未审批的 sig.yaml 变更一起结算
出入一:因果顺序反了。 原帖(以及我上一条回帖)写的是「权限文件自动同步 → Gitee 仓库权限自动下发」。Jenkins #5260 的实际顺序是:
01:50:40 PR action: approved/check_pass
01:50~ add xodjqe_admin_admin_admin to openkylin/<33 个仓库> ← 含 community
01:52 提交 9043624d「update permissions/gitee.json file」 ← 事后记录
01:52 合并 !2814
permissions/gitee.json 是 bot 写下的结果,不是下发权限的原因。这一条影响整改方案:只盯住权限文件是不够的,得盯住 sig-bot.py 的实做调用。
出入二:「轻量级 PR 自动合并」这个归因不成立。 Gitee 官方定义,轻量级 PR 只是「无需 Fork、网页端改文件即可提 PR」的便捷方式,仍须作者审核通过后合入。API 也显示只有 !2814 是轻量级 PR(head=N/A、标题带【轻量级 PR】),!2823/!2824/!2825 都是普通 fork PR(head=master)。所以「自动」不来自轻量级 PR,来自 openKylin 自己的 Jenkins / bot 配置。
2. 逐环节证据
| 环节 | 证据 | 出处 |
|---|---|---|
| 起点:零权限外部人 | 10-29 afd01746、11-04 b6877138 快照中该账号无任何权限记录;!2814 由 gfcbjh8 提交,标签 openkylin-cla/no,body 为空,diff 仅 maintainers +1 行 |
Gitee API pulls/2814;git show 01193873 |
| 入口无保护 | community 仓库 .gitee/ 下只有 PR/Issue 模板,无 CODEOWNERS/OWNERS;PR 模板要求填「主题 / 原因」,4 个 PR 均未按要求填写 |
.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md |
| 官方流程写得其实很严 | 《9_SIG组基本信息变更流程》:「如新增成员,则新成员需先签署cla」「技术委员会将提前审核相关信息,并在下一次例会上进行进一步沟通」;《2_SIG组加入流程》:「等待该 SIG 组核心成员投票表决通过后,申请 PR 审核通过,晋升……并获得相应的仓库权限」;《12_SIG组Owner增选、撤销规范》:2/3 投票 + 报备 TC,TC 一票否决 | openkylin/docs 三份文档(原文见第 7 节) |
| 实际门禁只有 CI 格式校验 | 4 个 PR 全部 openkylin-cla/no,bot 只发了一条「You have not signed the CLA」评论,未阻断;Sonar 报 1835 条 CodeSmell 却写「目前状态为: OK」;can_merge_check=false;PR 元数据里 assignees/testers 含申请人本人(!2825 的 testers 只有他一人);评论区无任何人类评审发言 |
Gitee API pulls/{2814,2823,2824,2825} + .../comments |
| 闸门 = PR 状态 | Jenkins #5275 第 31 行:sig-bot.py[line:571] - DEBUG: PR action: approved/check_pass;#5260 同样 |
jenkins.openkylin.top/job/sig-changed/5275/consoleText |
| 执行器 = sig-bot.py | 构建脚本 python3 sig-bot.py -r gitee;日志 line:511 remove X to openkylin/Y、line:519 add X to openkylin/Y;bot 源码仓库 openkylin/ci-bots(私有,匿名不可读) |
同上 |
| 合并前就写权限 | #5275:07:43:35 触发 → 07:44:07~07:45:05 逐条 add 63 个仓库 → 07:45:07 PR 才合并;#5260 同样先 add 33 个再合并 |
Jenkins 日志 + Gitee API merged_at |
| 权限文件只是快照 | permissions/README 原文:「保存openkylin仓库权限,由cibot根据sig与ppm信息自动生成」;packages/sample.yaml 里 maintainers[].name 直接就是 Gitee 账号 |
community 仓库 |
| 自举:首批就含治理仓库 | #5260 的 33 条 add 里明确有 openkylin/community,以及 oauth-backend/oauth-web/api-server/official-web;此后 11-06 08:40–09:18(UTC)五个 merge 的 author 是 gfc 本人 |
Jenkins #5260;git log --merges |
| 全量重算 | #5275 的 add/remove 清单里混着大量正常成员变更(add tjujiyan to qa2026、remove suchangzheng…),说明每次 PR 都把整个 SIG 体系重算一遍,之前埋进 kernel SIG 的 3 个包也在这次一起结算 → 33→96 |
Jenkins #5275 |
| 移除同样全自动 | 11-07 13:22 UTC 提交 7d57bd9c,13:25 快照归零 |
permissions/gitee.json 快照 |
3. 时间线(UTC;北京时间 = +8)
| 时间 | 事件 | admin 数 |
|---|---|---|
| 10-30 13:42 | !2814 创建(轻量级 PR、CLA=no、无说明、+1 行 Community SIG) |
0 |
| 11-06 01:50 | Jenkins #5260 approved/check_pass,bot 直接给 33 个仓库加 admin(含 community) |
0 → 33 |
| 11-06 01:52 | !2814 合并;cibot 写 9043624d 权限快照 |
33 |
| 11-06 08:40–09:18 | 他本人作为 merge author 连合 5 个 PR;08:43 自加 kernel SIG(未即时结算) | 33 |
| 11-07 06:44–07:43 | 自加 KDE(后撤)、qt、Networking;删 TC 三份 PDF 夹带进 PR | 33 |
| 11-07 07:43–07:45 | Jenkins #5275 approved/check_pass,逐条 add 63 个仓库 |
33 → 96 |
| 11-07 07:45 | !2823 合并 |
96 |
| 11-07 12:02 / 12:55 | !2824(2 分 26 秒)、!2825(1 分 53 秒)合并 → 104 → 115 |
115 |
| 11-07 13:22 / 13:25 | 7d57bd9c 移出;全量重算归零 |
0 |
| 11-09 02:49 | !2832 恢复 TC 纪要(= 北京时间 10:49) |
0 |
4. 是哪方面问题 —— 责任分层
| 层面 | 判定 | 依据 |
|---|---|---|
| Gitee 平台 | 无问题,按仓库配置正常执行 | 轻量级 PR 需人工审核;权限 API 由持有凭据的 bot 正常调用 |
| 治理规则(文档) | 规则本身完整甚至偏严(CLA + TC 审核 + 例会 + 2/3 投票 + README 同步修改) | openkylin/docs |
| 「规则 → 技术」的落地 | 核心缺陷:三道人工程序没有任何一道做成阻断式门禁 | CLA 只发评论、Sonar 状态 OK、can_merge_check=false、无 CODEOWNERS、sig/** 非受保护路径 |
| 权限模型设计 | 核心缺陷:低信任输入直通高权限输出 | maintainers[].name ≡ Gitee admin;全量重算;合并前就下发;首批含治理仓库自身;无阈值/延迟/告警 |
| CI / Bot | 未被入侵,严格按设计执行;但 dry-run 清单无人消费 | #5276/#5278 的 PR action: open 会打印 (dry run skip) 的 add 清单,本该是给审查人看的告警,1~2 分钟后即被 approved 实做 |
| 人(维护者) | 批量合并时未识别出异常 PR,属于流程后果,不是主因 | !2814 与 !2813/!2815/!2817/!2820 同批在 01:47–01:52 合并 |
安全分类:CWE-862 Missing Authorization / CWE-269 Improper Privilege Management;本质是 trust boundary 缺失 + 自动化信任链过长(“excessive automation / missing human-in-the-loop”)。
定性:这是社区治理 + DevSecOps 的授权设计/配置问题,不是代码漏洞,不是 0day,也不构成供应链投毒(原帖自己也确认未发现恶意代码)。
5. 为什么 8 天没人发现
permissions/gitee.json只记结果、不记原因,看不出「谁批准了这次加权限」。- 没有单账号维度的突增告警:0 → 33 → 96 → 104 → 115 全程无拦截。
- dry-run 的 add 清单混在几十条正常成员变更里(#5275 同时有
add tjujiyan、remove suchangzheng、add apenper等),63 条异常 add 并不显眼。 - 单看每个 PR 的 diff 都只有 +1 行,无害;危险来自「+1 行」与「bot 全量重算并直写 Gitee 权限」的组合,而这两者分属不同系统,没人把它们连起来看。
6. 整改清单(按优先级)
- 把
sig/**、packages/**设为受保护路径:必须 SIG owner + 技术委员会双人 approve,禁止申请人出现在assignees/testers(!2825 的 testers 只有他本人);引入 CODEOWNERS。 - CLA 从「提示」改为「阻断」:成员变更 PR 必须签 CLA,并强制填写模板里的「原因」、同步修改对应 SIG 的
README.md(官方流程已要求,加 CI 校验即可)。 - 切断「合并前下发」:
sig-bot.py的实做只允许在 PR 已合并 后由独立 job 触发。现在只要 PR 被打上approved/check_pass,哪怕最后被关闭/拒绝,权限也可能已经加上了。 - 给 add 操作加阈值与延迟:单账号单次 >N 个仓库、跨 SIG、或涉及
linux/curl/oauth-*/community等敏感仓,必须人工二次确认 + 冷却窗口。 - 把
community治理仓库自身从 SIG 自动授权范围中排除 —— 这是自举闭环的关键一环。 - 权限变更留审计日志(谁、依据哪个 PR、哪个 approve),接入告警;
permissions/gitee.json增加「变更来源」字段。 - 顺带:Jenkins
consoleText目前匿名可读,暴露内部仓库地址、凭据 IDjenkins-ssh-gitee与全量成员变更清单,建议收敛访问。
7. 附:官方文档原文(本次新查)
openkylin/docs → 1入门与参与/1_2社区贡献/04_社区兴趣小组(SIG)/
9_SIG组基本信息变更流程.md:「团队成员/成员权限变更……(如新增成员,则新成员需先签署cla),填写好相关信息后,技术委员会将提前审核相关信息,并在下一次例会上进行进一步沟通。」2_SIG组加入流程.md:「当开发者参与SIG组贡献满足以下 2 个及以上条件,可以申请SIG组核心成员:提交 5 个及以上有效 commit / 提出 5 个及以上有效 issue / 修复 5 个及以上 issue / 积极参与 SIG 会议……等待该SIG组核心成员投票表决通过后,申请PR审核通过,晋升为该SIG组核心成员并获得相应的仓库权限。」12_SIG组Owner增选、撤销规范.md:「需要三分之二或以上投赞同票……投票结果需报备技术委员会,技术委员会……拥有一票否决权。」community/README.md:「组内重大决策须由全体成员以超过 2/3 的票数投票决议并报备技术委员会。」community/permissions/README:「保存openkylin仓库权限,由cibot根据sig与ppm信息自动生成。」
对照实际:6 次自加全部是 1 file changed, 1 insertion(+),仅改 sig.yaml、不碰 README,commit message 统一为 update sig/<x>/sig.yaml.,无申请说明、无 CLA、无投票、无 TC 记录,最快 1 分 53 秒完成合并。而历史上正常的同类提交会写明事由(如 update: 申请qt维护者权限、qt-sig新增成员 renjianquan),部分还同步改了 sig/qt/README.md。
8. 可复现入口
https://gitee.com/api/v5/repos/openkylin/community/pulls/2814 # head=N/A、CLA=no、body 空
https://gitee.com/api/v5/repos/openkylin/community/pulls/2823 # assignees 含申请人本人
https://gitee.com/api/v5/repos/openkylin/community/pulls/2825 # testers 只有申请人本人
https://gitee.com/api/v5/repos/openkylin/community/pulls/2832 # TC 恢复,merged_at 2026-11-09T10:49:16+08:00
https://jenkins.openkylin.top/job/sig-changed/5260/consoleText # 33 条 add,含 community
https://jenkins.openkylin.top/job/sig-changed/5275/consoleText # approved/check_pass + 63 条 add
https://jenkins.openkylin.top/job/sig-changed/5276/consoleText # PR action: open, dry run skip
https://jenkins.openkylin.top/job/sig-changed/5278/consoleText
git clone --depth=1 https://gitee.com/openkylin/docs.git # 官方流程文档
Gitee commit 页有登录墙,但以上 API、Jenkins console、git clone 均匿名可读。