コマンド実行でシェルが怖いなら使わなければいいじゃない - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

2013-12-21(Sat) [sh][shell][security][escape] [更新履歴]

拡張 POSIX シェルスクリプト Advent Calendar 2013、21日目の記事です。 今日は、一部界隈に「シェル怖い」と感じてながら仕方なく使用している人や、 怖さを認識せず(できず?)に平気で使用している人が世に溢れているようなので、 シェルを避ける方法について紹介したいと思います。

記事の最後に、どうしてもシェルを利用したい場合にエスケープ等を気にせず安全・確実にパラメーターを渡すことができる実装案も示します。

あるプログラムから別のプログラム (OS コマンド) を実行する際、 プログラマーが意図してか意図せずかはさておき、 シェルが介在する場合があります。そこには様々な危険があります。 シェルをよく理解しうまく危険を回避するか、 理解できない/理解したくない/理解する必要がないならシェルを避けるべきです。

えー、3日前からチマチマ書き始めて、今現在 2013年12月21日の 15時。 まだ未完成で鋭意執筆中。仕上げる前に Twitter の TL でも消化するかなー。 おや? TL の様子が…。

この記事は 2013年12月21日には間に合わず、22日 20時に完成しました

「PHPだってシェル経由でないコマンド呼び出し機能が欲しい | 徳丸浩の日記」

https://twitter.com/ockeghem/status/414254447280148480

日記書いた>『このエントリはPHP Advent Calendar 2013 in Adventar の21日目です…』 PHPだってシェル経由でないコマンド呼び出し機能が欲しい | 徳丸浩の日記 http://blog.tokumaru.org/2013/12/php_21.html

…かっ、被りました。はい、ネタが被りましたよ。 しかも某方面お大御所の徳丸さんの記事です。 私が紹介したかった内容の半分以上はこの記事の内容の通りです! おかげ様でシェル介在による OS コマンドインジェクションの説明や代表的な回避方法、 PHP 方面の調査の手間が省けて助かりました(泣。

お薦めの記事なので是非読みましょう。 以下、読まれたことを前提で書きます。

なになにー?

*2 例外として、pcntl_fork および pcntl_exec を使ってコマンドを呼び出すと シェル経由にはなりませんが、 PCNTL関数の制限としてCGI版PHPを使わなければならないため、 通常のWebアプリケーションで利用するのは現実的ではありません。

えっ?!

*2 例外として、pcntl_fork および pcntl_exec を使ってコマンドを呼び出すと シェル経由にはなりませんが、 PCNTL関数の制限としてCGI版PHPを使わなければならないため、 通常のWebアプリケーションで利用するのは現実的ではありません。

ええっ?!!! (二度見)

何その欠陥。

信じられない。

PHP…おそろしい子!

閑話休題。以下、徳丸さんの記事と被っている点はご容赦を。

事の発端

そもそもは、 私の TL を賑せていた文字エスケープの話題にシェル関係の話が出てきたので、 少し突っ込みを入れてみたいと思い、この記事を書こうと思い立ちました。

そんな中で目にした Blog 記事:

「えすけーぷじゅうよう!!」を強調して言いたいからなのかシェルの理解が足りないからなのか、 意図がよくわからない文言やら説明が散見されますが、きりがないのでそれらはスルーします。 (シェルについては、なんで関係ない tcsh の話が出てくるんだとか、 位置パラメーター展開に $* 使うなとか、色々)

特に気になったのが以下の文章です。(強調は私によるもの)

OSコマンドはOSが提供するシェルで実行されます。 シェルはテキストインターフェースを持ち、 テキストでコマンドとオプションを受け取り実行します。 例示した脆弱なPHPプログラムの場合、 ユーザーからの入力に対しセキュリティ処理を一切してないため、 簡単にサーバーを乗っ取られる可能性があります。

「小難しい(?)シェルのメタ文字エスケープだけでなく、シェルを介さない OS コマンド実行方法も紹介すればいいのに」と思って記事を書き始めたわけですが、 この Blog を書かれた方は PHP 専門らしく、しかも PHP ではシェルを介さない OS コマンド実行ができない欠陥がある (条件に依るが)ので仕方ないのかもしれません。

さて、ようやく本題に入ります。

OS コマンドの実行方法

あるプログラム(プロセス)から外部の OS コマンドを実行するには、 単純に実行するだけでも次のような手順が必要です。少し面倒ですね。

  1. 子プロセスを生成する。(fork(2))
  2. 子プロセスは実行したいコマンドを起動する(成り代る)。 (exec(2))
  3. 親プロセスは子プロセスが終了するのを待ち、 子プロセスの終了コード (0〜255 の数値) と終了要因 (exit(2) したかシグナルを受けたか) を得る。(wait(2))

これ以外にも、コマンドの引数の準備や環境変数の設定、 コマンドの入力元(標準入力)と出力先(標準出力と標準エラー出力)の調整、 複数コマンドの組み合せや親プロセスとの通信(パイプライン、IPC)など、 要件に依ってはかなり面倒になります。

手軽な OS コマンド実行 API system(3) とその仲間たち

あなたの身近なところに OS コマンドを実行するのに非常に便利なものがあります。 それは何でしょうか? そう、シェルですね! もともとシェルは、ユーザーと OS との橋渡しをするためのプログラムとして誕生しました (要出典)。 普段、シェルを利用している人は、 シェルのおかげで息を吐くようにコマンドを実行していることと思います。

そのシェルを介してコマンドを実行するための API が system(3) です。 system(3) は C の関数です。 シェルを対話的に利用しているときと同様に、 C のプログラムから手軽にコマンドラインを実行することができます。 「コマンドライン」とはシェルスクリプトそのものです。

system(3) はシェルのコマンドライン文字列を引数にとり、/bin/sh コマンドを第一引数に -c、第二引数に コマンドライン文字列 を指定して実行するだけの単純なインターフェイスです。 手元の環境の日本語マニュアルより抜粋:

SYSTEM(3) Linux Programmer’s Manual

名前

system - シェルコマンドの実行

書式

#include <stdlib.h>
int system(const char *command);

説明

system()command で指定したコマンドを /bin/sh -c command の形で実行する。指定したコマンドが終了すればこの関数も終了する。 コマンド実行中は、SIGCHLD はブロックされ、SIGINTSIGQUIT は無視される。

C 以外の各種言語にも system(3) と同名あるいは同等の API が用意されています。 この中には引数の与え方によって動作が変わる実装があります。 詳細は各言語のマニュアルで確認してください。

system(3) の問題点 (= シェルの仕様の理解不足 != シェルの問題点)

私は system(3) と同類の API には大きな問題点が 2つあると認識しています。

  1. シェルを介してコマンドが実行されることが認知されていない。
  2. シェルを介することは知っているがシェルのことを理解していない。

system(3) の利点であり時に欠点となるのがシェルによる柔軟さです。 シェルはもともと対人用に作成された経緯もあり(要出典)、 シェルが受け付けるコマンドラインはかなり自由に書けます。 それはシェルスクリプトそのものであり、単一のコマンド実行だけでなく、 複数のコマンド実行や様々な構文が書けます。

シェルとの対話に慣れた人であれば、シェルにコマンド入力(対話)するとき、 あまり細かいことを意識せずにコマンド名、オプション、そのほか引数を入力し、 必要であればシェルの特殊文字(メタ文字)をエスケープしていることでしょう。 しかし、シェルをプログラムから利用する場合はどうでしょうか。 あなたの書いたプログラムは、あなたの期待するコマンド名、オプション、引数を、 ちゃんとシェルに伝えられていますか?

その困難さを示すお題を一つ。お好きな言語で system(3) 相当の API あるいはシェルの /bin/sh -c 'コマンドライン' を利用して、 引数をそのままコマンドとして実行するプログラムを書いてみてください。 シェルだと概ねこんな実装になります。

#!/bin/bash
##
## 引数をそのままコマンドとその引数として実行するスクリプト。
## ただし、外部の /bin/sh (あるいはそれ相当) を利用すること。
## (未完成)
##

typeset -a args

for arg in "$@"; do
  ##
  ## ここで適宜 $arg を加工する処理を書いてください。
  ##
  args+=($arg)
done

/bin/sh -c "${args[*]}"  ## あるいは eval "${args[*]}"

もし「簡単だよ! できたよ!!」という方は、そのスクリプトの引数に echo/*; echo Hacked!!!!!!!!!!!!!! を与えてみてください。 期待通りに動きましたか? 結果 /* ; echo Hacked!!!!!!!!!!!!!! 以外の何かを表示したら、そのスクリプトはバグっています。

実際はこの程度の要件であればそれほど難しくはないのですが、 シェルスクリプトの動的構築なんてできれば避けたいお題です。

#!/bin/bash
##
## 引数をそのままコマンドとその引数として実行するスクリプト。
## ただし、外部の /bin/sh (あるいはそれ相当) を利用すること。
##

typeset -a args

for arg in "$@"; do
  args+=("'${arg//'/'\\''}'")
done

/bin/sh -c "${args[*]}"  ## あるいは eval "${args[*]}"

OS コマンド実行にシェルの介在を無くす

ごちゃごちゃと御託を並べましたが、そもそも何の話でしたっけ? 「あるプログラムから別のプログラム (OS コマンド) を実行」ですよね。 シェルは要件に挙がってないのです。 シェルが必要なく余計なのであれば、 シェルが介在しないコマンド実行方法を利用すればいいのです。簡単ですね。

幸い、大抵の言語には system(3) と同程度に簡単に OS コマンドを起動する方法が用意されています。 それを利用しましょう。 お題を先の「引数をそのままコマンドとその引数として実行するプログラム」 として、各種言語での実装例を紹介します。

まずシェルの実装例です。さすがコマンド実行に特化したシェル、実に簡単ですね。 (余談ですが $*, "$*", $@, "$@" の違いがわからない人はシェルスクリプトは書かないでください。怖いです。 こちらの解説をどうぞ )

#!/bin/sh
## 引数をそのままコマンドとその引数として実行するスクリプト

"$@"

Perl の実装例です。Perl の system() はシェルを介在させないコマンド実行方法が複数あるのですが、 この例の書き方が確実です。

#!/usr/bin/perl
## 引数をそのままコマンドとその引数として実行するスクリプト

use strict;
use warnings;
use POSIX;

my $cmdstatus = system {$ARGV[0]} @ARGV;
exit(POSIX::WEXITSTATUS($cmdstatus));

Ruby の実装例です。Ruby の system() は Perl のそれを模倣しているようです。 シェルを介在させないコマンド実行方法が複数あります。 Perl 同様に、この例の書き方が確実です。

#!/usr/bin/ruby
## 引数をそのままコマンドとその引数として実行するスクリプト

ret = system([ARGV[0], ARGV[0]], *ARGV[1..-1])
exit($?.exitstatus)

Python の実装例です。os.systemsystem(3) と同じくシェルを介するコマンド実行しかできまないので別の API を用います。

#!/usr/bin/python
## 引数をそのままコマンドとその引数として実行するスクリプト

import subprocess
import sys

exit(subprocess.call(sys.argv[1:]))

PHP は CGI 版か CLI 版でないとできないそうです、残念! 省略!!

それでもシェルを利用したいときは? - パラメーターの環境変数渡し

ときにはプログラムからシェルスクリプトを実行したいときがあるかもしれません。 安全・確実に実現するにはどうしたらよいでしょうか。実はこれは非常に簡単です。

ポイントはこれだけです。

  1. シェルスクリプトは静的な文字列として定義する。
  2. パラメーターは環境変数で渡す。
  3. エスケープ不要!

要は SQL のプリペアードステートメントみたいなものです。

https://twitter.com/satoh_fumiyasu/status/410630655966343168

.@riywo どうしても system(3) 等でシェルを利用したい場合はパラメーターはコマンドラインに直接記述せずに環境変数を経由するといいです。エスケープ不要で楽だし安全。 $ENV{param}=';rm -rf /';system('cmd "$param"');

Ruby による実装例を示します。 第一引数にネームサービス (NSS) のデータベース名、 第二引数にデータベースの検索パターン(拡張正規表現)、 第三引数に sed のスクリプトをとるコマンドです。 (実用的な例でなくてすみません)

#!/usr/bin/ruby

def getent_egrep_sed(db, pattern, script)
  env = {
    'db' => db,
    'pattern' => pattern,
    'script' => script,
  }

  system(env, 'getent -- "$db" |egrep -- "$pattern" |sed -- "$script"')
end

getent_egrep_sed(*ARGV[0..2])

シェルスクリプト内では、 環境変数を受け取る(変数展開)ときにダブルクォートすることが重要です。 ダブルクォート内で展開されたパラメーターはパス名展開やワード分割などの対象とならないため、そのままの値がコマンドの引数に渡されます。

余談ですが、コマンドにオプションでない引数を渡す場合は、 オプション終端オプション -- 以降に指定することも重要です。 これはシェルを介するかどうかには関係ありませんが、 コマンドに引数を与えるときは常に注意しましょう。

実行例を示します。

$ ruby nss_egrep_sed.rb passwd '/zsh$' ''
…シェルが zsh のユーザーエントリーを表示…
$ ruby nss_egrep_sed.rb group ':[^:]+$' 's/:.*:/ /'
…メンバーがいるグループ名とメンバーを表示…
$ ruby nss_egrep_sed.rb '";touch /tmp/x;"' '`touch /tmp/y`' '$(touch /tmp/z)'
…エラーになるだけでコマンドインジェクションは発生しない…

大文字の環境変数はコマンドの動作に影響を与えるものが存在するため、 小文字の環境変数名を利用しましょう。変数名に「プログラム名_」などのように 接頭辞を付けると区別しやすくなりお勧めです。

環境変数はスレッドローカル変数ではないので、 マルチスレッドで動くプログラムの場合は競合に注意してください。 Ruby の system() のように、起動するコマンドの環境変数を別の名前空間で指定できる実装であれば問題ありません。


ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。