コマンドインジェクションと引数インジェクションの防止
信頼できない入力を適切にクォート、検証し、配列で渡すことで、単語分割やevalを利用したインジェクションを防ぎます。
「コマンドインジェクションと引数インジェクションの防止」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。
Bash でインジェクション攻撃が発生する理由
Bash は強力な接着剤型言語です。テキストをカーネルや他のプログラム、サブシェルへ直接渡します。しかし、信頼できない入力が検証やクォートなしでコマンドに渡された瞬間、その強力さが弱点になります。
ほぼすべての Bash インジェクションは、次の 2 つの根本原因によって発生します。
- ワード分割: クォートされていない変数は空白(
IFS)で分割され、1 つの論理的な値が複数のシェルトークンになります。 - グロブ展開:
*、?、[などの文字は、コマンドが実行される前にシェルによって展開されます。
ファイル名、ユーザー名、URL パラメーター、環境変数を攻撃者に制御されると、これら 2 つの仕組みを悪用して任意のコマンドを実行したり、ファイルを読み取ったり、権限を昇格させたりできます。
このレッスンでは、こうした脆弱性がどのように現れるかを具体的に示し、さらに重要な点として、正しいクォート、入力検証、配列による引数の受け渡しによってどのように排除するかを説明します。
ワード分割: 見えにくい脅威
Bash はクォートされていない変数を見つけると、その値を $IFS に含まれる文字(デフォルトではスペース、タブ、改行)で分割します。1 つに見える引数が複数の引数になります。
以下のスクリプトを実行し、スペースを含むファイル名が rm に渡される 2 つの別々の引数になることを確認してください。
#!/usr/bin/env bash
# Dangerous: unquoted variable
FILE='important file.txt'
# Create the file so the demo is self-contained
touch "$FILE"
echo "Files before:"
ls
# BUG: rm sees TWO arguments: 'important' and 'file.txt'
# If 'important' does not exist, rm prints an error but continues.
rm $FILE # <-- unquoted, word-split happens here
echo "Files after (unquoted rm):"
ls常にクォートする: 防御的 Bash の第一原則
ワード分割に対する最も簡単で効果的な防御策は、変数展開を常にダブルクォートすることです。
"$var"— スペース、タブ、改行を保持したまま、必ず 1 つのトークンに展開されます。'literal'— シングルクォートです。展開は一切行われないため、固定文字列に適しています。- ワード分割やグロブ展開を明示的に必要とする場合を除き、
$varをクォートなしで使用してはいけません。
以下のスクリプトは、前の例を安全にしたものです。
#!/usr/bin/env bash
set -euo pipefail
FILE='important file.txt'
touch "$FILE"
echo 'Files before:'
ls
# SAFE: double-quotes keep the filename as one token
rm "$FILE"
echo 'Files after (quoted rm):'
lsグロブインジェクション: * が武器になるとき
クォートされていない変数は、パス名展開(グロブ)の対象にもなります。ユーザーが制御する入力に * や ? が含まれていると、コマンドの実行前に Bash がファイルシステムに対して展開します。
典型的な攻撃経路は、Web フォームで PATTERN=* が設定され、スクリプトが cp $PATTERN /tmp/leak/ を実行するケースです。この場合、カレントディレクトリ内のすべてのファイルがコピーされます。
対策は同じです。変数をダブルクォートしてください。クォートされた "$PATTERN" はそのまま渡され、シェルによるグロブ展開は行われません。
#!/usr/bin/env bash
set -euo pipefail
# Simulate attacker-supplied input
PATTERN='*'
mkdir -p /tmp/safe_demo_src /tmp/safe_demo_dst
touch /tmp/safe_demo_src/secret1.txt /tmp/safe_demo_src/secret2.txt
cd /tmp/safe_demo_src
# UNSAFE: glob expands, copies every file
# cp $PATTERN /tmp/safe_demo_dst/
# SAFE: pattern is treated as a literal filename
cp "$PATTERN" /tmp/safe_demo_dst/ 2>&1 || echo 'No file named literally "*" — attack neutralised'
rm -rf /tmp/safe_demo_src /tmp/safe_demo_dstクォートされていない位置パラメーターによる引数インジェクション
呼び出し元から引数を受け取るスクリプトは、インジェクションの格好の標的です。各位置パラメーター($1、$2 など)は、使用する場所ごとにクォートする必要があります。
特に危険なのは、$@ や $* をクォートなしで別のコマンドに渡すパターンです。
"$@"— 各位置パラメーターを、個別にクォートされた別々のワードとして展開します。常にこの形式を使用してください。$@または$*をクォートなしで使用 — ワード分割とグロブの影響を受けます。"$*"— すべてのパラメーターを 1 つのワードに結合します(通常、意図した動作ではありません)。
#!/usr/bin/env bash
set -euo pipefail
# Safe wrapper: forward all arguments quoted
grep_wrapper() {
local pattern="$1"
shift
# "$@" preserves each file argument as one token
grep -rn "$pattern" "$@"
}
# Usage: ./script 'error msg' /var/log/syslog '/path with spaces/app.log'
echo 'Searching current script for "safe":'
grep_wrapper 'safe' "$0"eval と未検証入力によるコマンドインジェクション
eval は引数をシェルコードとして再解析します。信頼できないデータが eval に渡ると、任意のコマンドが実行される可能性があります。
よくある危険なパターン:
eval "$user_input"eval echo \$$var(間接的な変数参照)- ユーザーデータを
bash -c "$input"経由で渡す
原則: 信頼できない入力を eval や bash -c に渡してはいけません。安全な Bash の代替手段を使用してください。
- 間接展開:
eval echo \$$varnameの代わりに${!varname}を使用します - 動的なキーと値の検索には連想配列を使用します
- 生成したコマンド文字列の代わりに関数を使用します
#!/usr/bin/env bash
set -euo pipefail
# Simulated attacker-supplied variable name
VARNAME='PATH; echo INJECTED'
# UNSAFE: eval lets attacker run 'echo INJECTED'
# eval "echo \$$VARNAME"
# SAFE: indirect expansion only resolves valid variable names
# First validate that VARNAME is a legal identifier
if [[ "$VARNAME" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "Value: ${!VARNAME}"
else
echo "ERROR: invalid variable name: '$VARNAME'" >&2
exit 1
fi入力検証: ブラックリストよりも許可リスト
既知の危険な文字を拒否する方法(拒否リスト)は脆弱です。攻撃者は、見落としたエンコーディングや文字を見つけ出します。その代わりに、許可リストを使い、安全だと確認した文字だけを受け入れてください。
Bash での許可リスト戦略:
- 正規表現による照合:
[[ "$input" =~ ^[A-Za-z0-9_-]+$ ]] - パターンによる照合:
case "$input" in [A-Za-z0-9]*) ... ;; esac - 列挙値のチェック: 有効な値を固定の集合と比較します
入力がスクリプトに入った境界で、つまりコマンドに触れる前に検証してください。
#!/usr/bin/env bash
set -euo pipefail
validate_username() {
local name="$1"
# Allowlist: only lowercase letters, digits, underscore, hyphen; 1-32 chars
if [[ ! "$name" =~ ^[a-z0-9_-]{1,32}$ ]]; then
echo "ERROR: invalid username '${name}'" >&2
return 1
fi
echo "Username accepted: $name"
}
validate_username 'alice' # OK
validate_username 'bob_smith-2' # OK
validate_username 'root; rm -rf /' # REJECTED
validate_username '../etc/passwd' # REJECTED配列を使って安全に引数を渡す
フラグを条件付きで追加したり、入力をループ処理したりするなど、コマンドを動的に組み立てる必要がある場合は、文字列を連結するのではなくBash 配列を使用してください。
文字列連結では構造がすべて失われますが、Bash 配列では各引数が独立した要素として保持され、シェルによる再解析も行われません。
- 宣言:
args=() - 追加:
args+=(--flag "$value") - 実行:
command "${args[@]}"
"${args[@]}" は各要素を、"$@" と同じように、個別にクォートされた別々のワードとして展開します。
#!/usr/bin/env bash
set -euo pipefail
# Build a find command safely with an array
BASEDIR='/tmp'
USER_PATTERN='*.log' # could come from user input (validate first!)
MAX_DAYS=7
cmd=(find "$BASEDIR" -type f -name "$USER_PATTERN" -mtime "+${MAX_DAYS}")
# Optionally add -delete only when requested
DELETE=false
if [[ "$DELETE" == 'true' ]]; then
cmd+=(-delete)
fi
echo "Running: ${cmd[*]}"
"${cmd[@]}"-- 区切り文字: フラグインジェクションからの保護
正しくクォートされた引数でも、- で始まっているとオプションフラグとして誤解釈されることがあります。file='-rf .' のときの rm "$file" を考えてみましょう。クォートによってワード分割は防げますが、rm は依然として -rf をフラグとして解釈します。
POSIX の慣例である -- は、ほとんどの GNU/BSD ユーティリティに対してオプションの終わりを示します。-- の後ろにあるものはすべて位置引数として扱われ、フラグとして解釈されることはありません。
#!/usr/bin/env bash
set -euo pipefail
# Simulate a dangerous filename supplied by the user
FILENAME='-rf /tmp/safe_demo_target'
mkdir -p /tmp/safe_demo_target
touch /tmp/safe_demo_target/keep_me.txt
echo 'Files before:'
ls /tmp/safe_demo_target/
# UNSAFE (flag injection — do NOT uncomment in real systems):
# rm "$FILENAME"
# SAFE: -- ends option processing; filename is treated literally
rm -- "$FILENAME" 2>&1 || echo "No such file (attack neutralised): $FILENAME"
echo 'Files after:'
ls /tmp/safe_demo_target/
rm -rf /tmp/safe_demo_targetSQL と外部ツール向けの入力サニタイズ
Bash スクリプトからデータベース CLI(psql、mysql)、ユーザー指定 URL を扱う curl、または同様のツールを呼び出す場合は、さらに 2 つの層が必要です。
- パラメーター化クエリ: ユーザーデータを SQL 文字列に直接埋め込んではいけません。
psqlでは-vを、curlでは--data-urlencodeを使って値を渡します。 - データとコードの分離: リテラルの書式文字列を指定した
printfを使用し、ユーザー入力を決して書式文字列にしてはいけません。
以下の例では、ユーザーが指定した値を SQL テキストから完全に分離したまま、安全に PostgreSQL にクエリを実行します。
#!/usr/bin/env bash
set -euo pipefail
# Validate first: only allow alphanumeric usernames
USERNAME="${1:-alice}"
if [[ ! "$USERNAME" =~ ^[a-z0-9_]{1,32}$ ]]; then
echo 'ERROR: invalid username' >&2
exit 1
fi
# UNSAFE:
# psql -c "SELECT * FROM users WHERE name = '$USERNAME';"
# SAFE: pass value as a psql variable, never inside the SQL text
# psql -v username="$USERNAME" -c 'SELECT * FROM users WHERE name = :username;'
# Demonstrate the principle with printf (safe format string usage)
printf 'Query would use username: %s\n' "$USERNAME"堅牢化チェックリスト: すべてを組み合わせる
本番環境向けの安全な Bash スクリプトでは、このレッスンで学んだすべての手法を一貫した多層防御として組み合わせます。以下は最小限ながら完全な堅牢化テンプレートです。
set -euo pipefail— エラー時に終了し、未設定の変数をエラーとして扱い、パイプの失敗を伝播させます。- 入口で検証する — 外部入力はすべて、どのコマンドに触れる前にも許可リストで検証します。
- すべてクォートする —
"$var"、"$@"、"${array[@]}"を使用します。分割が必要な場合を除き例外はありません。 - 動的なコマンドの組み立てには配列を使用します。
- ユーザーが指定したファイル名や文字列を渡すときは、引数の前に
--を付けます。 - 信頼できないデータに対して
evalを決して使用せず、間接参照には${!var}を使用します。 - 権限を制限する — 必要最小限の権限でスクリプトを実行し、ユーザー入力を受け取るスクリプト内での
sudoは避けます。
#!/usr/bin/env bash
# Hardened template — safe argument injection prevention
set -euo pipefail
IFS=$'\n\t'
#--- 1. Validate inputs at the boundary ---
SEARCH_DIR="${1:-}"
PATTERN="${2:-}"
[[ -z "$SEARCH_DIR" || -z "$PATTERN" ]] && { echo 'Usage: script <dir> <pattern>' >&2; exit 1; }
[[ ! "$SEARCH_DIR" =~ ^[A-Za-z0-9/_.-]+$ ]] && { echo 'ERROR: unsafe directory path' >&2; exit 1; }
[[ ! "$PATTERN" =~ ^[A-Za-z0-9._-]+$ ]] && { echo 'ERROR: unsafe pattern' >&2; exit 1; }
#--- 2. Build command with an array ---
cmd=(find -- "$SEARCH_DIR" -type f -name "$PATTERN")
#--- 3. Execute — no string interpolation, no eval ---
echo "Executing: ${cmd[*]}"
"${cmd[@]}"理解度チェック: クォートとインジェクション防止
このレッスンの重要な概念を理解できているか確認しましょう。
レッスンのまとめ: コマンドインジェクションと引数インジェクションの防止
Bash で入力を安全に扱うための防御ツールキットを一通り学びました。
- ワード分割とグロブ展開は、安全でない変数をインジェクションの経路に変える根本的な仕組みです。
- すべての変数(
"$var"、"$@"、"${arr[@]}")をダブルクォートして、両方の脅威を抑制します。 - 引数を転送するときは
"$@"を使用し、クォートなしの$@や$*は決して使用しません。 - ユーザーが指定したファイル名の前に
--を付け、フラグインジェクションを防ぎます。 - 外部入力はすべて、コマンドに到達する前に正規表現によるガード(
[[ $v =~ ^pattern$ ]])で許可リストに基づいて検証します。 - 動的なコマンドは配列(
cmd+=()→"${cmd[@]}")で構築し、文字列連結は決して使用しません。 evalとbash -c "$input"を排除し、安全な間接展開には${!varname}を使用します。- 堅牢化の基本として、スクリプトの先頭には必ず
set -euo pipefailとIFS=$'\n\t'を記述します。
これらのプラクティスをすべてのスクリプトの最初の行から一貫して適用すれば、インジェクション系の脆弱性に対する Bash の攻撃対象領域をほぼゼロまで減らせます。
よくある質問
「コマンドインジェクションと引数インジェクションの防止」レッスンは無料ですか?
はい。「コマンドインジェクションと引数インジェクションの防止」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。
「コマンドインジェクションと引数インジェクションの防止」で何を学びますか?
信頼できない入力を適切にクォート、検証し、配列で渡すことで、単語分割やevalを利用したインジェクションを防ぎます。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
DevOps Bootcampを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「コマンドインジェクションと引数インジェクションの防止」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このDevOps Bootcampレッスンでコードを書いて実行できますか?
はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- コマンドインジェクションと引数インジェクションの防止
- シークレットの安全な取り扱いと環境の衛生管理
- 最小権限実行とsudoの適切な運用
- ShellCheckによる静的解析と監査