ビルドのメタデータを実行コードから分離する

ShellBeginner
オンラインで実践に進む

はじめに

投稿者はコードの変更案にタイトルと説明を付けます。ビルドジョブはそのメタデータからプレビュー成果物を作ります。継続的インテグレーション(CI)とは、変更が届いた際にこうしたジョブを自動実行する仕組みです。runner はジョブを実行するプログラムです。ここでは用意された小さなローカル runner を自分で起動します。

人物とイベントはすべて合成データです。runner はネットワークもデプロイ認証情報もない使い捨てコンテナで、本物の Bash を実行します。release.conf の production はローカルのテスト値で、何もデプロイしません。この演習は文字列置換とシェル実行を扱い、ホスト型 CI のトリガーや権限は再現しません。CI Workshop ターミナルが保守用の作業領域です。Topology は処理の流れを示し、更新時に実際の実行チャネルを表示します。

通常のビルドを実行し、スクリプトインジェクションを追跡し、短い 2 ファイルを編集してメタデータの正確な保持を再検証します。基本的なターミナル操作に慣れていれば十分です。必要な JSON と Bash の構文は使用前に説明します。

通常のビルドを実行する

このステップでは、問題の調査前に期待される業務結果を確認します。投稿者のタイトルがそのまま成果物になり、リリースチャネルは preview のままになるはずです。

このステップを始めるには、ラボ画面の CI Workshop タブを選択してください。

CI Workshop タブを使います。pwd は現在のディレクトリを表示し、ls はファイルを一覧表示します。作業領域は /home/labex/project/ci-workshop です。

次のコマンドを実行する前に、CI Workshop タブに切り替えてください。

pwd
ls

cat はテキストファイルを表示します。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 はシェル命令を読みます。TITLE="..." は変数への代入で、= の前後に空白は置きません。"$TITLE" は値を 1 引数として展開し、空白と改行を保持します。printf '%s' は改行を追加せず文字列を出力します。単一引用符は %s をそのまま扱わせます。> は出力をファイルに送り、既存内容を置き換えます。セミコロンは 2 つの命令を区切ります。set -euo pipefail は一般的なコマンド失敗、未定義変数、パイプライン失敗で停止させますが、インジェクションは防ぎません。

ci は用意された runner コマンドです。run には .json なしのイベント名と、新しい出力ディレクトリ名を渡します。各実行は新しい preview 設定から始まります。

ci run ordinary first

channel=preview と exit=0 が期待値です。終了コード 0 は Bash が正常終了した意味です。show で生成スクリプト、成果物、設定を確認します。

ci show first

成果物は JSON 文字列 "Improve search results" として表示されます。これにより後で不可視の改行も表現できます。これは実際のファイル内容で、模擬の成功フラグではありません。最後の節には preview が必要です。Topology タブに切り替えて更新すると、first 行が同じ設定ファイルを読み取ります。

first を指定して run を繰り返すと、既存ディレクトリのエラーが出ます。過去の証拠を上書きしないための仕様です。first-again など新しい名前を選んでください。失敗した実行も、調査用に生成スクリプトと build.log を runs/NAME に残します。

危険な置換を追跡する

このステップでは、投稿者のデータが実行可能なコードに変わり、隔離されたデモ実行内でチャネルを変更する様子を観察します。

このステップを始めるには、ラボ画面の CI Workshop タブを選択してください。

流れはイベント → ジョブ → 生成シェルソース → Bash → 成果物です。危険なのは、Bash が引用符を解析する前の置換です。二重引用符は解析済み命令内の変数展開を保護できますが、解析前にソースへ直接挿入される値は保護できません。

用意された無害なデモイベントを読みます。

次のコマンドを実行する前に、CI Workshop タブに切り替えてください。

cat events/hostile.json

title には二重引用符、セミコロン、printf、さらにセミコロンと # が含まれます。Bash では引用符が元の文字列を終了し、セミコロンが別の命令を始め、# が行末までをコメントにします。この printf が変更するのは、この実行の合成 release.conf だけです。JSON の \" は文字列中の引用符を表すもので、後からシェルソースへ挿入した際の安全性は保証しません。

新しい作業領域で実行します。

ci run hostile demo
ci show demo

runner が preview で初期化したにもかかわらず、channel=production になります。generated.sh のセミコロン間に挿入された printf を探してください。Bash は実際にそれを実行しました。成果物には Update しか残らない場合があります。残りがタイトルデータではなく構文になったためです。これが修正対象です。

demo と first は別のディレクトリで、どちらも job.json や build.sh を変更しません。Topology タブに切り替え、ページを更新して実際のチャネルを比較します。production 行はテスト設定の変更であり、本物のリリースではありません。イベント文字列を保守用シェルで手動実行しないでください。

通常実行と脆弱性のデモ実行のチャネルを比較する Topology

一般的なリスクは GitHub のスクリプトインジェクション解説に記載されています。ここでは GitHub Actions より小さなジョブ形式と独自プレースホルダーを使いますが、置換が解析より先に起こる仕組みは共通です。

値をデータとして渡す

このステップでは、投稿者のテキストをシェルソースに入れず、空白、引用符、複数行を保持します。

このステップを始めるには、ラボ画面の CI Workshop タブを選択してください。

環境変数は、新しいプロセスに与える名前付き文字列です。runner の env マッピングはシェルの代入文を生成せず、直接その値を設定できます。JSON では波括弧内の別のオブジェクトで表します。BUILD_TITLE に {{event.title}} を設定し、実行時に Bash が "$BUILD_TITLE" を展開するようにします。展開された値の中の引用符、セミコロン、$()、バッククォートは新しい命令として再解釈されません。eval や bash -c に値を渡すと再解析されるため、使わないでください。

here-document で短い 2 ファイルを置き換えます。cat > filename <<'JSON' は、JSON だけの行まで後続行をファイルに書きます。終端名を引用すると、保守用シェルによる $ などの展開を防げます。最後の終端行も含め、コードブロック全体をコピーしてください。ファイル全体を置き換えるため、入力ミスからの復旧にも使えます。

次のコマンドを実行する前に、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}} に変えるだけで、シェル変数を引用する規則は同じです。チャレンジのリリースノートでこの仕組みを再利用します。

ci test normal は提出されたジョブとスクリプトを変更せず、通常、句読点、複数行のイベントで実行します。使い捨ての一時領域で成果物のバイトを比較し、preview を確認します。一時領域だけを削除し、提出ファイルと保存済み実行は変更しません。

ci test normal

各入力で exact metadata; channel=preview が表示されます。句読点の除去、改行の平坦化、固定タイトル、全イベントの拒否、ビルドの省略は、この通常機能の検査に失敗します。エラーは入力名を示します。該当ファイルを読み、2 ファイルを修正して再試行してください。

ビルドの境界を再検証する

このステップでは、同じ修正済みジョブが正常なメタデータを保ち、実演した副作用を防ぐことを確認します。

このステップを始めるには、ラボ画面の CI Workshop タブを選択してください。

新しい出力名で悪意ある入力を再実行します。

次のコマンドを実行する前に、CI Workshop タブに切り替えてください。

ci run hostile repaired-hostile
ci show repaired-hostile

成果物には悪意あるタイトルの全体がテキストとして残る必要があります。JSON 文字列の表示にセミコロンと引用符が含まれ、release.conf は preview のままです。古い demo は別の領域に属するため変更されません。

別の入力には $() とバッククォートが含まれます。シェルソースにあれば、どちらもコマンド置換の構文です。二重引用符内で展開した環境変数の値に含まれる場合は、データのままです。入力を読みます。

cat events/substitution.json

同じ提出ファイルで 5 入力すべてを検証します。

ci test all

ordinary、quotes、multiline、hostile、substitution のすべてで正確なメタデータと preview が保持され、最後に一時領域の削除メッセージが出ます。実際の Bash 実行と出力ファイルを検査するもので、ログの一文や架空の secure=true は採点しません。完了検査は同じ一時テストを繰り返し、ジョブ、スクリプト、保存済み実行を変更しません。

悪意ある入力だけ失敗する場合、シェル内にイベントのプレースホルダーが残っていないか、変数を再解析していないか確認します。句読点や複数行が失敗する場合、二重引用符付きの変数を printf '%s' で出力しているか確認します。扱いにくい入力を削除しないでください。正当なメタデータの保持も要件です。Topology タブに切り替え、ページを更新し、repaired-hostile と過去の demo を比較します。

修正後の実行で preview チャネルが維持されることを示す Topology

まとめ

投稿者のメタデータをローカル runner から実際の Bash まで追跡し、隔離実行で設定変更を再現しました。環境マッピングと引用符付き展開でデータ境界を修正し、正確な出力検査で句読点、複数行、プレビューチャネルを保持しました。次は同じ考え方をリリースノートに応用します。