シェルスクリプトのセキュリティ設計|クォート徹底・コマンドインジェクション対策・evalを避ける書き方

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトのセキュリティ設計|クォート徹底・コマンドインジェクション対策・evalを避ける書き方
「シェルスクリプトにユーザーの入力をそのまま渡したら、意図しないコマンドが実行されてしまった」
「ファイル名にスペースが入っていただけで、バックアップ処理が途中で壊れた」

シェルスクリプトの脆弱性は、難解な暗号理論や複雑なプロトコル知識とは無縁のところで生まれる。クォートの省略、外部入力の無検証使用、eval の安易な利用——いずれも「ちょっとした書き方の癖」が招く問題だ。

この記事では、シェルスクリプトのセキュリティ設計の基本を具体的に解説する。RHEL 9.4 / Ubuntu 24.04 LTS の bash 5.x 環境で動作確認した実例をもとに、クォートの徹底からコマンドインジェクション対策、eval を避ける書き方、set -u・mktemp・IFS リセットまでを体系的にカバーする。

この記事のポイント

・変数は必ずダブルクォートで囲む("$var" が安全な基本形)
・外部入力はバリデーション後にコマンドへ渡す(インジェクション対策の核心)
・eval は使わない。配列・declare・case 文で安全に代替できる
・set -u・IFSリセット・mktemp でスクリプト全体を堅牢化する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

シェルスクリプトの脆弱性はなぜ起きるか

シェルは「ユーザーが入力したテキストをそのままコマンドとして解釈する」設計になっている。これはパイプや変数展開を自在に組み合わせられる強みであると同時に、外部からの入力を不用意に扱うと「意図しないコマンドが実行される」脆弱性に直結する。

脆弱性の主な原因は4つに分類できる。

クォート省略:変数を " で囲まないと、スペース・タブ・glob 文字が予期せぬ単語分割やファイル展開を引き起こす
外部入力の無検証使用:ユーザー入力やファイル名をそのままコマンドに渡すと、インジェクション攻撃の足がかりになる
eval の安易な使用:eval は渡した文字列をシェルコマンドとして再評価する。攻撃者が制御できる文字列を eval に渡すと任意コマンドを実行できる
一時ファイルの安全でない作成:/tmp/tmpfile のような固定名で作ると、シンボリックリンク攻撃(TOCTOU)の対象になる

それぞれ具体的に対策を見ていこう。

クォートの徹底:ダブルクォートで変数を囲む

1. クォートなしで何が起きるか

以下のスクリプトで問題を確認してみる。

# 悪い例:クォートなし filename="my document.txt" rm $filename # bash は "my" と "document.txt" の2引数として解釈する

変数 $filename は "my document.txt" だが、クォートなしだと bash は単語分割を行い、rm my document.txt として解釈する——つまり mydocument.txt の2ファイルを削除しようとする。ファイル名にスペースが含まれると、この挙動が深刻なバグを生む。

glob 展開でも同様の問題が起きる。

# 悪い例:glob 展開の意図しない動作 search_term="*.conf" ls $search_term # カレントディレクトリの全 .conf ファイルが展開されてしまう

2. ダブルクォートで変数を守る

解決策は単純だ——変数参照は必ず " で囲む。

# 良い例:ダブルクォートで囲む filename="my document.txt" rm "$filename" # スペースを含むファイル名を1つの引数として扱う search_term="*.conf" ls "$search_term" # glob 展開されずリテラル文字列として渡される

サブシェル展開 $() もダブルクォートで囲む。

# サブシェル展開のクォート output="$(command)" # OK:結果を変数に格納 echo "$output" # 改行が保持される # シングルクォートは変数展開を行わない echo '$filename' # リテラル "$filename" と出力される echo "$filename" # 変数の値が展開される

原則:シングルクォート(リテラル文字列用)とダブルクォート(変数展開を含む文字列用)を意識して使い分けることが、クォートの基本だ。変数展開・コマンド置換が含まれる箇所はすべてダブルクォートで囲む習慣をつけてほしい。

コマンドインジェクション対策:外部入力を安全に扱う

1. 外部入力をそのまま渡す危険

Web アプリケーションが外部からの入力をシェルに渡す処理や、スクリプトが引数・環境変数・ファイルの内容を受け取る場面で、コマンドインジェクションのリスクが生まれる。

# 危険な例:外部入力をそのままコマンドに渡す user_input="$1" ls $user_input # $1 に "; cat /etc/shadow" が渡された場合、セミコロン以降が実行される

この例では $1 に悪意ある文字列("; cat /etc/shadow" など)が渡されると、予期しないコマンドが実行される可能性がある。

2. バリデーションと安全な渡し方

最初のルールは「外部入力を信頼しない」こと。入力値を使う前に、必ず内容をチェックする。

#!/bin/bash # 良い例:入力バリデーション user_input="$1" # 英数字・ハイフン・アンダースコアのみ許可 if [[ ! "$user_input" =~ ^[a-zA-Z0-9_-]+$ ]]; then echo "エラー: 不正な入力です" >&2 exit 1 fi # バリデーション済みの値を使用 ls /var/log/"$user_input"

ファイルパスを扱う場合は、絶対パスに変換してホワイトリストで制御する。

#!/bin/bash # ディレクトリ限定の安全なファイル処理 BASE_DIR="/var/app/data" user_file="$1" # realpath で絶対パスに変換してから確認 real_path="$(realpath -m "${BASE_DIR}/${user_file}")" # BASE_DIR 配下かチェック(パストラバーサル対策) if [[ "$real_path" != "${BASE_DIR}/"* ]]; then echo "エラー: 許可されたディレクトリ外へのアクセスです" >&2 exit 1 fi cat "$real_path"

実際のサーバー(web01.example.local)でこのスクリプトを動作させた際の出力例:

# 正常なアクセス $ ./safe_cat.sh config.json {"version": "1.0", "env": "production"} # パストラバーサル試行 $ ./safe_cat.sh ../../etc/passwd エラー: 許可されたディレクトリ外へのアクセスです

シェルスクリプトをゼロから体系的に学ぶなら

セキュリティ設計・エラー処理・自動化実装まで、現場で通用するシェルスクリプトの書き方を宮崎が解説するコース。独学でつまずいている方に特に好評です。

シェルスクリプト講座の詳細を見る >>

evalを避ける:なぜ危険でどう書き換えるか

1. evalが危険な理由

eval はシェルスクリプトの中でも特に危険なコマンドだ。渡した文字列をシェルが再度解釈するため、ユーザー入力やファイルの内容が含まれる文字列を eval に渡すと、任意コマンドの実行が可能になる。

# 非常に危険な例 user_input="$1" eval "echo $user_input" # $1 に "$(cat /etc/shadow)" を渡すとそのまま実行される

$1$(cat /etc/shadow) のような文字列を渡すと、eval がシェルコマンドとして実行し、機密ファイルの内容が読み取られてしまう。

2. 安全な書き換えパターン

eval を使いたくなる代表的なパターンと、安全な代替手段を確認しよう。

パターン1:変数名を動的に作りたい

# eval を使いたくなるパターン(危険) prefix="user" eval "${prefix}_name=Tanaka" # 安全な代替:連想配列(bash 4.0 以降) declare -A user_info user_info[name]="Tanaka" echo "${user_info[name]}" # Tanaka

パターン2:コマンド名を動的に選択したい

# eval を使いたくなるパターン(危険) action="start" eval "${action}_service" # 安全な代替:case 文でホワイトリスト制御 case "$action" in start) start_service ;; stop) stop_service ;; *) echo "不明なアクション: $action" >&2; exit 1 ;; esac

パターン3:コマンドラインを動的に組み立てたい

# 安全な代替:配列でコマンドを組み立てる cmd=(ls -la) if [[ "$show_hidden" == "true" ]]; then cmd+=(--all) fi cmd+=("$target_dir") # 配列要素がそれぞれ1つの引数として渡される "${cmd[@]}"

eval を書いたら「これは配列・case・declare で書き直せないか」と必ず自問する癖をつけてほしい。

その他のセキュリティプラクティス(set -u・IFSリセット・一時ファイル安全化)

1. set -u で未定義変数を検出する

デフォルトでは、未定義の変数を参照しても bash は空文字列として扱う。これが意図しない動作を引き起こすことがある。

#!/bin/bash set -eu # -e: コマンド失敗で即終了、-u: 未定義変数でエラー # $backup_dir が未定義のまま展開されると危険 # set -u なし → 空文字に展開されてシステム直下が対象になりかねない # set -u あり → "bash: backup_dir: unbound variable" で即停止 echo "バックアップ先: ${backup_dir}"

set -eu をスクリプト先頭に置くだけで、未定義変数起因のバグを早期に検出できる。

2. IFSを明示的にリセットする

IFS(Internal Field Separator)は単語分割の区切り文字を制御する変数だ。外部環境から変更された IFS が引き継がれる可能性があるため、スクリプト冒頭で明示的にリセットするのが安全だ。

#!/bin/bash # IFS を安全にリセット(スペース・タブ・改行) IFS=$' ' # ファイルを1行ずつ読む場合も IFS を制御する while IFS= read -r line; do # $line の内容が予期せず分割されない echo "$line" done < /etc/hosts

while IFS= read -r line のパターンは、ファイル読み込み時の標準イディオムだ。IFS=(空)にすることで行頭・行末の空白が削除されず、-r でバックスラッシュエスケープを無効化する。

3. mktempで一時ファイルを安全に作る

一時ファイルを固定名で作ると、攻撃者に先にシンボリックリンクを置かれる可能性がある(TOCTOU 問題)。mktemp を使ってランダムな名前を生成することで、この問題を防ぐ。

#!/bin/bash set -eu # 危険な例:固定名の一時ファイル # /tmp/myapp.tmp ← 攻撃者が事前にシンボリックリンクを作れる # 安全な例:mktemp でランダムな一時ファイルを生成 tmpfile="$(mktemp /tmp/myapp.XXXXXXXX)" # スクリプト終了時(正常・異常問わず)に確実に削除 trap 'rm -f "$tmpfile"' EXIT # 一時ファイルへの書き込みと利用 process_input > "$tmpfile" validate_output < "$tmpfile"

trap 'rm -f "$tmpfile"' EXIT を組み合わせることで、スクリプトが正常終了・異常終了のどちらでも確実に一時ファイルを削除できる。

セキュアなシェルスクリプトのチェックリスト

スクリプトを書き終えたら、以下の項目を確認する習慣をつけてほしい。

チェック項目 NG パターン OK パターン
変数のクォート rm $filename rm "$filename"
外部入力のバリデーション ls $1(無検証) 正規表現チェック後に ls "$1"
eval の使用 eval "$user_cmd" 配列・case 文・declare で代替
未定義変数対策 デフォルト(未定義=空文字) set -u をスクリプト冒頭に配置
IFS の明示化 環境依存の IFS を引き継ぐ IFS=$' \t\n' でリセット
一時ファイルの作成 /tmp/myapp.tmp(固定名) mktemp /tmp/myapp.XXXXXXXX
一時ファイルの削除 スクリプト末尾のみで削除 trap 'rm -f "$tmpfile"' EXIT
パストラバーサル対策 ユーザー入力をパスに直接連結 realpath + BASE_DIR プレフィックス検証

シェルスクリプトを体系的に習得したい方へ

今回解説したセキュリティ設計だけでなく、実用的な自動化スクリプトの書き方・エラーハンドリング・cron 連携まで、現場で使えるシェルスクリプトをゼロから学べるコースを提供しています。独学で行き詰まりを感じている方は、ぜひ一度ご覧ください。

シェルスクリプト講座の詳細を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。