一个自包含、可复现的环境,用于演示 CVE-2025-23211——Tandoor Recipes 中的一个服务端模板注入(SSTI)漏洞,该漏洞可导致远程代码执行。该仓库会启动存在漏洞的版本,展示漏洞利用过程,然后启动已修补的版本,并展示相同的输入被拒绝。
| CVE | CVE-2025-23211 |
| 安全公告 | GHSA-r6rj-h75w-vj8v |
| 类别 | 服务端模板注入(Jinja2),CWE-94 / CWE-1336 |
| CVSS 3.1 | 9.9(严重)- AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| 受影响版本 | Tandoor Recipes <= 1.5.23 |
| 修复版本 | 1.5.24 |
Tandoor Recipes 是一个开源、自托管的膳食计划和食谱管理应用程序。后端是 Django(Python),由 Gunicorn 提供服务;前端是 Vue.js 单页应用;数据存储在 PostgreSQL 中。
Tandoor 允许用户在食谱的说明文本中编写 Jinja2 模板表达式,该功能旨在用于诸如缩放食材数量之类的任务。这些说明会在服务端渲染。
说明文本由 cookbook/helper/template_helper.py 中的 render_instructions() 处理。在受影响的版本中,该文本被传递给 Jinja2 的默认 Template() 并在没有沙箱的情况下渲染:
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
由于默认的 Jinja2 环境暴露了 Python 的对象图,能够编辑食谱的已认证用户可以注入一个模板表达式,从空元组遍历到 subprocess.Popen,并在服务器上执行任意命令。默认容器以 root 身份运行,因此该命令以 root 权限执行。
说明在 Jinja2 渲染之前会经过一个 Markdown 处理步骤,该步骤会剥离原始下划线。因此,该漏洞利用对下划线进行十六进制编码(\x5f\x5f),并使用 attr() 过滤器代替点号表示法。参见 payloads.txt。
compose-vulnerable.yml Tandoor 1.5.23(存在漏洞)加 PostgreSQL
compose-patched.yml Tandoor 1.5.24(已修补)加 PostgreSQL
payloads.txt 两个概念验证载荷及预期输出
enumerate-popen.txt 用于在其他镜像上查找 subprocess.Popen 索引的辅助工具
本仓库中的证据是在以下配置上捕获的。任何带有 Compose v2 或更高版本的较新 Docker 发行版都能复现相同的结果。
启动存在漏洞的堆栈:
docker compose -f compose-vulnerable.yml up -d
等待 web 容器日志显示 worker 启动,然后打开
http://localhost:8080。
注册第一个账户。第一个用户将成为超级用户。在提示时设置 Space 名称。
创建一个新食谱。在 Steps > Instructions 字段中,粘贴 payloads.txt
中的 Payload 1,然后保存。
打开食谱查看。渲染后的说明显示
<class 'subprocess.Popen'>,确认注入到达了 Python 内部。
使用 Payload 2 重复。查看食谱现在会运行 whoami 并显示 root。
直接确认进程用户:
docker compose -f compose-vulnerable.yml exec web_recipes whoami
停止存在漏洞的堆栈:
docker compose -f compose-vulnerable.yml down
启动已修补的堆栈:
docker compose -f compose-patched.yml up -d
使用相同的载荷重复相同的步骤。食谱视图现在不再执行,而是显示
Could not parse template code. Error: Undefined Error。
要重置所有内容并从空数据库开始,请运行
docker compose -f compose-vulnerable.yml down -v。-v 标志会删除
命名卷,因此下次运行会从头重新创建数据库。
该补丁(提交 e6087d5)将未沙箱化的 Template() 替换为 Jinja2 的
SandboxedEnvironment:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
沙箱覆盖了 Jinja2 的属性查找,以禁止访问不安全的属性,例如 __class__、__base__、__subclasses__ 和 __getitem__。
没有这些属性,载荷就无法从普通对象到达 subprocess.Popen,
因此表达式会失败而不是执行。
该漏洞利用通过 attr() 过滤器而非点号表示法访问属性,
因此在修补后的代码中,被阻止的访问表现为 UndefinedError
而不是 SecurityError;两者都由沙箱版本处理。
本仓库出于教育目的复现一个已公开披露且已修补的漏洞。 请仅针对此处提供的本地容器运行它。 不要针对您不拥有或未获授权测试的系统。