让构建元数据远离可执行代码

ShellBeginner
立即练习

介绍

贡献者提出一项代码变更,并填写标题和说明。构建任务将这些元数据生成预览产物。持续集成(CI)是指变更到来时自动运行这类任务。runner 是执行任务的程序。在这里,你将自行启动一个预置的精简本地 runner。

所有人物和事件都是合成的。runner 在一次性容器中运行真实 Bash,没有网络或部署凭据。release.conf 中的 production 只是本地测试值,绝不会触发部署。本实验模拟文本替换和 shell 执行,不模拟托管 CI 平台的触发器或权限。CI Workshop 终端是维护工作区;Topology 解释数据流,并在刷新时显示实际运行通道。

你将运行普通构建、追踪脚本注入、编辑两个短文件,并重新测试元数据是否完整保留。具备基本终端操作经验即可;需要的 JSON 和 Bash 语法都会在使用前讲解。

运行普通构建

本步骤先展示预期业务结果,再调查问题:贡献者标题应原样成为产物,发布通道应保持 preview。

在实验界面选择 CI Workshop 标签页,开始本步骤。

切换到 CI Workshop 标签页。pwd 显示当前目录,ls 列出目录中的文件。工作区是 /home/labex/project/ci-workshop。

运行以下命令前,请切换到 CI Workshop 标签页。

pwd
ls

cat 显示文本文件。JSON 对象用花括号包住命名字段,冒号将字段名和值连接起来。JSON 字符串使用双引号,字符串中的 \n 表示换行。阅读普通事件:

cat events/ordinary.json

它的 title 是 Improve search results,body 是多行说明。这些值由贡献者提供,属于数据,构建不能将它们当作命令信任。

阅读精简任务定义及其脚本:

cat job.json
cat build.sh
项目 在本地 runner 中的含义
script 要运行的 Bash 文件名,目前是 build.sh
env 将进程环境变量名映射到字符串,目前是空对象 {}
{{event.title}} 在 Bash 启动前,将这个占位符替换为事件标题
{{event.body}} 对说明字段进行相同替换
artifact.txt 构建生成的元数据产物
release.conf 每次运行初始化为 preview 的通道

Bash 读取 shell 指令。TITLE="..." 为 shell 变量赋值,= 两侧不能有空格。"$TITLE" 将变量展开为一个参数,保留空格和换行。printf '%s' 输出一个字符串,不额外添加换行;单引号让 %s 保持字面值。> 将输出重定向到文件并替换原有内容。分号分隔两条命令。set -euo pipefail 让常见命令失败、未定义变量和管道失败停止脚本,但不能防止注入。

ci 是预置的本地 runner 命令。run 接收事件名(不含 .json)和一个新的输出目录名。每次运行都从新的 preview 配置开始。运行普通事件:

ci run ordinary first

预期看到 channel=preview 和 exit=0。退出码零表示 Bash 成功结束。使用 show 查看生成的脚本、产物和配置:

ci show first

产物以 JSON 字符串 "Improve search results" 显示,以便稍后展示不可见的换行。这是真实文件内容,不是模拟成功标记。最后一节必须显示 preview。切换到 Topology 标签页并刷新,它的 first 行读取同一个配置文件。

再次用 first 执行 run会出现目录已存在错误,这是有意保留旧证据的设计。请选择 first-again 等新名称。失败的运行也会在 runs/NAME 中保留生成脚本和 build.log,供诊断使用。

追踪危险的文本替换

本步骤观察贡献者数据如何变成可执行代码,并在一次隔离演示中改变通道。

在实验界面选择 CI Workshop 标签页,开始本步骤。

数据流是事件 → 任务 → 生成的 shell 源码 → Bash → 产物。危险发生在 Bash 解析引号之前的替换阶段。双引号可以保护已解析命令中的变量展开,却无法保护在解析前直接插入源码的原始值。

阅读预置的无害演示事件:

运行以下命令前,请切换到 CI Workshop 标签页。

cat events/hostile.json

title 中包含双引号、分号、printf、另一个分号和 #。在 Bash 中,该引号结束原字符串,分号启动新命令,# 让本行剩余内容成为注释。其中的 printf 只修改本次运行的合成 release.conf。JSON 中的 \" 用于在字符串中编码双引号,不能让之后插入 shell 源码的引号自动变安全。

在新工作区运行事件:

ci run hostile demo
ci show demo

虽然 runner 初始化了 preview,你却会看到 channel=production。在 generated.sh 中找到分号之间插入的 printf,Bash 确实执行了它。产物可能只剩 Update,因为其余内容变成了语法,不再是标题数据。这正是待修复的问题。

demo 和 first 是独立目录,两个运行都不会更改 job.json 或 build.sh。切换到 Topology 标签页并刷新页面,比较实际通道。production 行仅代表测试配置被改动,不代表真实发布。不要在维护 shell 中手动执行事件文本。

Topology 比较普通运行与漏洞演示运行的实际通道

这一类风险见 GitHub 的脚本注入说明。本地占位符和任务格式有意比 GitHub Actions 精简;实验展示的是共同的“先替换、后解析”机制。

将值作为数据传递

本步骤让贡献者文本不再进入 shell 源码,并保留空格、引号及多行内容。

在实验界面选择 CI Workshop 标签页,开始本步骤。

环境变量是传给新进程的命名字符串。runner 的 env 映射可直接设置它,不需要生成 shell 赋值语句。在 JSON 中,映射是花括号中的另一个对象。让 BUILD_TITLE 来自 {{event.title}},然后由 Bash 在运行时展开 "$BUILD_TITLE"。Bash 不会把展开值中的引号、分号、$() 或反引号重新解释成命令。不要使用 eval 或把值交给 bash -c,它们会再次解析。

我们将用 here-document 替换两个短文件。cat > filename <<'JSON' 把后续行写入文件,直到遇到仅含 JSON 的一行。带引号的结束标识阻止维护 shell 展开文本中的 $ 等语法。复制完整代码块,包括最后的结束标识。它会替换整个文件,因此也适合在打错后恢复。

运行以下命令前,请切换到 CI Workshop 标签页。

cat > job.json <<'JSON'
{
  "script": "build.sh",
  "env": {
    "BUILD_TITLE": "{{event.title}}"
  }
}
JSON

接着替换脚本,BASH 标识起同样的字面写入作用。脚本直接使用环境变量,不再包含贡献者占位符。

cat > build.sh <<'BASH'
set -euo pipefail
printf '%s' "$BUILD_TITLE" > artifact.txt
printf 'channel='; cat release.conf
BASH

用之前学过的 cat 读回两个文件。缺少逗号或花括号会产生 JSON 错误;环境名拼错会产生未定义变量错误。重新写入对应的完整代码块再试。旧运行属于旧脚本,因此使用新名称:

ci run ordinary repaired
ci show repaired

生成的脚本现在包含 "$BUILD_TITLE",而不是标题字符。产物仍是 Improve search results,通道仍为 preview。

测试前阅读标点和多行样例:

cat events/quotes.json
cat events/multiline.json

JSON 字符串里的 \n 代表真实换行,最后一个 \n 也必须保留。O'Brien 中的撇号和标题内的双引号都是合法数据。同样的映射可传递多行 body:字段引用改为 {{event.body}},shell 变量加双引号的规则不变。挑战中的发布说明将复用这个机制。

ci test normal 在一次性临时目录中,用未经修改的提交任务和脚本处理普通、标点及多行事件。它逐字节比较产物并检查 preview,最后仅删除临时目录,不改提交文件或已保存运行。

ci test normal

每个样例应显示一行 exact metadata; channel=preview。去掉标点、压平换行、硬编码标题、拒绝所有事件或跳过构建都会无法通过这个正常功能检查。失败信息会指出样例名;阅读对应文件并修正两个文件后重试。

重新测试构建边界

本步骤确认同一个修复后的任务既保留正常元数据,也阻止已演示的副作用。

在实验界面选择 CI Workshop 标签页,开始本步骤。

用新的输出名称再次运行恶意样例:

运行以下命令前,请切换到 CI Workshop 标签页。

ci run hostile repaired-hostile
ci show repaired-hostile

产物现在必须包含完整恶意标题的字面文本。JSON 字符串显示中能看到分号和引号,release.conf 保持 preview。旧的 demo 证据属于另一个工作区,不会改变。

另一个样例包含 $() 和反引号;它们出现在 shell 源码中时都表示命令替换。在带双引号展开的环境变量值中,这些字符仍是数据。阅读该样例:

cat events/substitution.json

用同一份提交文件运行全部五个样例:

ci test all

ordinary、quotes、multiline、hostile 和 substitution 均应保留精确元数据及 preview,最后显示临时目录清理消息。检查依据是真实 Bash 执行和输出文件,不是日志中的一句话或虚构的 secure=true。内置完成检查会重复这些临时测试,不修改任务、脚本和已保存运行。

若只有恶意样例失败,查看 shell 文件中是否仍有事件占位符,或变量是否被重新解析。若标点或多行样例失败,检查是否对带双引号的变量使用 printf '%s'。不要删除难处理的输入:保留合法元数据也是目标。切换到 Topology 标签页并刷新页面,比较 repaired-hostile 和历史 demo。

Topology 显示修复后的运行保留 preview 通道

总结

你追踪贡献者元数据如何经本地 runner 进入真实 Bash,在隔离运行中复现了配置被修改,并用环境映射和带引号展开修复数据边界。精确输出测试确认正常标点、多行内容和预览通道均被保留。接下来,把同样的思路用于发布说明任务。