シェルスクリプトでCGIスクリプトを作る方法を解説する。HTTP GETリクエストを使ってブラウザからCGIスクリプトに文字を送り込み、CGIスクリプト側でそれに応じて動的に表示を変えるという処理を扱う。
動作にはApache HTTP Serverが動くUNIX系の環境を利用する。ここでは「CGIスクリプトを作る(1)―Webサーバへのデータは標準出力で渡す」でセットアップしたMac OS Xを使用する。FreeBSDやLinuxで実行する場合には適宜読み替えて準備してほしい。
セットアップしたMac OS X環境では/Library/WebServer/CGI-Executables/にCGIスクリプトを置くと、http://localhost/cgi-bin/hoge.cgiとURLを指定してCGIスクリプトを起動できる。/Library/WebServer/CGI-Executables/にいちいち移動するのは面倒なので、リスト1のように作業アカウントのホームディレクトリにcgi-bin という名前でシンボリックリンクを張って、そこにスクリプトを置くようにしてある。
リスト1: ホームから簡単にアクセスできるようにする
$ cd $ ln -s /Library/WebServer/CGI-Executables/ ./cgi-bin $ cd cgi-bin $ sudo chown ueda:wheel . $
リスト2: 環境
$ uname -a Darwin uedamac.local 12.3.0 Darwin Kernel Version 12.3.0: Sun Jan 6 22:37:10 PST 2013; root:xnu-2050.22.13~1/RELEASE_X86_64 x86_64 $ apachectl -v Server version: Apache/2.2.22 (Unix) Server built: Dec 9 2012 18:57:18 $
GETリクエストは、HTTPでCGIスクリプトに文字列を渡すための方法の1つ。ブラウザなどでURLを指定するときに、後ろに文字列をくっつけてCGIスクリプトにその文字列を送り込むといった使い方がされている。
リスト3に、シェルスクリプトで実例を示す。
リスト3: GETで文字列を受け取り表示するCGIスクリプト
$ cat echo.cgi #!/bin/sh -xv echo "Content-type: text/html" echo echo "$QUERY_STRING" $
これを ~/cgi-bin/ に置いて、ブラウザから次のように実行する。
図1: GETで送った文字列を表示
これを解説すると、まず、ブラウザに打った文字列
http://localhost/cgi-bin/echo.cgi?gets!!
これはecho.cgiにgets!!という文字列をGETリクエストで渡すという意味になる。
文字列を送りつけられたCGIスクリプトの方は、なんらかの方法でその文字列を受け取ることになる。この文字列はQUERY_STRINGという環境変数に入っているのでそれを使う。つまり、リスト2のようにHTTPヘッダをつけてただ$QUERY_STRINGをechoするだけで、ブラウザにむけてGETで受け取った文字列を出力できる。
環境変数QUERY_STRINGを使うときは、よほど特殊な事情がない限り、6行目のようにダブルクォートで囲む。囲まないと、次のようになってしまう。
図2: $QUERY_STRING のダブルクォートを除いて * を送り込む
これはインタラクティブシェルでのルールと同じである。アスタリスクがパス名展開の対象になってしまっている。リスト4のようにターミナルで作業すると理解しやすい。
リスト4: 端末上でのクォート有無の実験
$ A="*" $ echo $A dame.cgi download_xlsx.cgi ...(略) $ echo "$A" * $
環境変数QUERY_STRINGはだたの文字列なので、コマンドインジェクションなどが実施されることはない。たとえ$QUERY_STRINGのクォートがないとしても、echoの後ろの変数はただ文字列に変換されるだけで実行はされない。
図3: セミコロンの後ろにコマンドをインジェクション
環境変数QUERY_STRINGに限らず、変数そのものがコマンドとして使われていたり、組み込みコマンドevalで評価するといったコードになっている場合には注意が必要。たとえばリスト5はコーディングミスで環境変数QUERY_STRINGの文字列がコマンドとして実行されることになる。
リスト5: GETで受けた文字列を実行してしまうパターン
$ cat yabai1.cgi #!/bin/sh -xv echo "Content-type: text/html" echo "$QUERY_STRING" $
次のように動作を確認できる。
$ curl http://localhost/cgi-bin/yabai1.cgi?ls dame.cgi download_xlsx.cgi echo.cgi ... $
次のようにevalを経由しても評価対象として実行されてしまう。このようなコーディングは危険といえる。
$ cat yabai2.cgi #!/bin/sh -xv echo "Content-type: text/html" echo eval "$QUERY_STRING" $ curl http://localhost/cgi-bin/yabai2.cgi?ls dame.cgi download_xlsx.cgi echo.cgi ... $
他にもいろいろセキュリティ脆弱性につながる書き方はあるが、少なくともこのくらいは知っておいて予防しておけばよいだろう。
ここではサーバ監理用のWebページを開発する。ページからコマンドを呼び出すことができるCGIスクリプトを作る。
まず、リスト6のhtmlファイルを作る。これをCGIスクリプトで読み込み、sed等で加工することで動的にHTMLを出力する。
リスト6: com.html
$ cat com.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>オレオレマシーン情報</title>
</head>
<body>
<form name="FORM" method="GET" action="./com.cgi">
コマンド:
<select name="COM">
<option value="0">cat /etc/hosts</option>
<option value="1">top -l 1</option>
</select>
<input type="submit" value="ポチ" />
</form>
<pre>
<!--RESULT-->
</pre>
</body>
</html>
$
次にリスト7のCGIスクリプトを用意し、このhtmlファイルを表示する。デバッグ用に後ろの方でecho "$QUERY_STRING"しておく。
リスト7: com.cgi
$ cat com.cgi #!/bin/sh -xv htmlfile=/Users/ueda/cgi-bin/com.html ###表示 echo "Content-type: text/html" echo cat $htmlfile #デバッグ用 echo "$QUERY_STRING" $
これでブラウザからcom.cgiを呼び出し、セレクトボックスから項目を選び、ボタンを押してみる。図4のように左下にGETで送った文字列が表示される。
図4: フォームで送信される文字列
COM=1のCOMはセレクトボックスについた名前(com.htmlのname="COM"の部分)、=より右側は選んだ項目のvalue の値になっている。valueの値からブラウザでどの項目が選ばれたか分かるので、セレクトボックスに書かれたコマンドをそのまま実行すればよいということになる。番号とコマンドの対応表のファイルをどこかに置いておけばよい。
このときOpen usp Tukubaiのmojihameというコマンドを使う。com.htmlを次のように書き換える。
リスト7: mojihameに対応したcom.html
<select name="COM"> <!--COMLIST--> <option value="%1">%2</option> <!--COMLIST--> </select> $
次にcom.cgiをリスト8のように書き換える。これでブラウザには$tmp-listに書かれたコマンドが番号(行番号)をつけられてセレクトボックスにセットされる。コマンドのリストは外部のファイルでもよいのだが、説明のためにヒアドキュメントで作っている。
先にリスト8について、本題と関係ない細かい部分を説明しておくと、リスト2行目の-vxはシェルスクリプトの実行ログの出力を行うためのオプション。4行目のexec 2>は、このスクリプトのエラー出力をファイルにリダイレクトするためのコマンドになっている。11行目から14行目のヒアドキュメントは、FINとFINの間に書いたものを標準出力に出力するという動きをする。FINは始めと終わりで対になっていればEOFとかHOGEとかでもかまわない。
リスト8: コマンドのリストをcom.htmlにはめ込むためのcom.cgi
$ cat com.cgi
#!/bin/sh -xv
exec 2> /tmp/log
PATH=/usr/local/bin:$PATH
htmlfile=/Users/ueda/cgi-bin/com.html
tmp=/tmp/$$
cat << FIN > $tmp-list
cat /etc/hosts
top -l 1
echo test_test _
FIN
###表示
echo "Content-type: text/html"
echo
sed 's/_/\\_/g' $tmp-list |
tr ' ' '_' |
awk '{print NR,$1}' |
mojihame -lCOMLIST $htmlfile -
#デバッグ用
echo "$QUERY_STRING"
rm -f $tmp-*
exit 0
$
mojihameの部分だけ抜き出すと、まず、22行目のawkの後のパイプにはリスト9のようなデータが流れる。行番号がついて、スペースは_、_は\_にエスケープされている。Open usp Tukubaiのコマンドは空白区切りのデータを受け付けるので、それに合わせてデータを変換している。
リスト9: エスケープ後のコマンドのリスト
1 cat_/etc/hosts 2 top_-l_1 3 echo_test\_test_\_
これで2列のデータになる。これをmojihameに入力すると、COMLISTで挟まれた部分がレコードの数だけ複製され、1列目がリスト@@@の%1、2列目がリスト@@@の%2にはめ込まれる。エスケープされた文字は戻る。mojihameが出力するHTMLのうち、セレクトボックスの部分をリスト10に示す。
リスト10: com.cgi が出力するHTMLの一部
<select name="COM"> <option value="1">cat /etc/hosts</option> <option value="2">top -l 1</option> <option value="3">echo test_test _</option> </select>
mojihameは慣れると便利なコマンドだ。
com.cgiはセレクトボックスから数字を受け取るが、数字だけしか受け取れないわけではない。リスト11のようにcurl等を使っても、ブラウザでURLの後ろを細工しても邪悪な文字列を送る事ができる。
リスト11: com.cgiに直接GETでデータを渡す
$ curl "http://localhost/cgi-bin/com.cgi?reboot" (略) </html> reboot $
ここで取り上げた例だと単に数字のみを受け付ければよいので、tr(1)を使って次のようにGETリクエストで送られてきた文字列を受け取るようにする。tmp=/tmp/$$の行の下あたりに次のコードを加える。
NUM=$(echo "$QUERY_STRING" | tr -dc '0-9')
tr(1)のオプション-dは文字(この例では0から9までの数字)を消すという意味だが、-cをつけると意味が反転するためリスト12のような挙動を示す。UTF-8であれば日本語が混ざっても問題ない。
リスト12: tr(1)を使い指定の文字「以外」を削除
$ echo 'COM=1aewagああ2' | tr -dc '0-9' 12$
このように12だけ残る。改行も消える。これで行番号が変数NUMに入るので、あとはリストのコマンドを実行すればよい。
成果物となるcom.cgiをリスト13に示す。変数に文字列が入っていなかったり、中間ファイルができなかったりというところでバグが出るので多少慣れが必要だ。たとえばCOMにコマンドが入らないと23行目でエラーが出るので、21行目でCOMに:(なにもしないコマンド)を入れるなど、細かい調整を加えている。
リスト13: com.cgi完成品
#!/bin/sh -xv
exec 2> /tmp/log
PATH=/usr/local/bin:$PATH
htmlfile=/Users/ueda/cgi-bin/com.html
tmp=/tmp/$$
######実行可能コマンドリスト######
cat << FIN > $tmp-list
cat /etc/hosts
top -l 1
echo test_test _
FIN
######コマンドの実行######
#番号受け取り
NUM=$(echo "$QUERY_STRING" | tr -dc '0-9')
#指定された行を取得
COM=$(awk -v n="$NUM" 'NR==n' $tmp-list)
#COMが空なら : を入れておく
[ -z "$COM" ] && COM=":"
#実行
$COM > $tmp-result
######HTML出力######
echo "Content-type: text/html"
echo
#エスケープ処理
sed 's/_/\\_/g' $tmp-list |
tr ' ' '_' |
#行番号をつける
awk '{print NR,$1}' |
#出力 >>> 1:行番号 2:コマンド
mojihame -lCOMLIST $htmlfile - |
#コマンド実行結果をはめ込み
filehame -lRESULT - $tmp-result
rm -f $tmp-*
exit 0
$
完成品では、もう1つfilehameというコマンドを使っている。これはあるファイルの間に別のファイルの中身を差し込むコマンドで、次のように使う。
リスト14: filehameの使い方
$ cat file1 ===参加者=== ATT ===以上=== $ cat meibo 山田 里中 殿間 $ filehame -lATT file1 meibo ===参加者=== 山田 里中 殿間 ===以上=== $
実行結果を図5に示す。
図5: 実行結果
ボタンを押すとセレクトボックスの選択結果が戻ってしまうがこれもコマンドで対応できる。Open usp Tukubaiのformhameというコマンドを使えばよい。
HTTP GETリクエストを使ってCGIスクリプトに文字列を送り込む方法を説明し、ブラウザからコマンドを実行するアプリケーションを作った。com.htmlとcom.cgiを合わせても60行程度にしかならない。いつも端末を叩いたりシェルスクリプトを書いたりしている人が覚えておくと、特に何か試作するときに威力を発揮することだろう。
Software Design 2013年8月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【20】CGIスクリプトを作る(2)―GETで文字列を取得する」より加筆修正後転載