ぐらめぬ・ぜぷつぇんのはてダ(2007 to 2011)

2007年~2011年ごろまで はてなダイアリー に書いてた記事を引っ越してきました。

cgi.force_redirect って何?

実はこれがよく分からないので、怖くて今までPHPCGIで動かしたことが無い、というへたれです。
ただいつまでも逃げてらんないし、何よりCGIで動かせばPHP4/5を同時に使えるので少し調べてみました。
何が怖いかというと、php.iniには

; cgi.force_redirect is necessary to provide security running PHP as a CGI under
; most web servers.  Left undefined, PHP turns this on by default.  You can
; turn it off here AT YOUR OWN RISK
; **You CAN safely turn this off for IIS, in fact, you MUST.**

とか書いて有るじゃないですか。AT YOUR OWN RISK ですよ?
なのに、「PHPCGIを動かすには〜」系でググったページではそこかしこで

cgi.force_redirect = 0
にしたら動きました!

的な結果だけで、そもそもこの設定は何の為にあるのか、何故デフォルトで有効化されているのか、などなど何にも書かれてないのが多いわけです。
しかも、php.iniにはこの設定のちょっと下に cgi.redirect_status_env という設定がコメントアウトされていて、

; if cgi.force_redirect is turned on, and you are not running under Apache or Netscape
; (iPlanet) web servers, you MAY need to set an environment variable name that PHP
; will look for to know it is OK to continue execution.  Setting this variable MAY
; cause security issues, KNOW WHAT YOU ARE DOING FIRST.

"KNOW WHAT YOU ARE DOING FIRST."なのです。

というわけで、まずはPHPマニュアル。

PHP: CGI バイナリとしてインストール - Manual

ここでは4つのケースに分けて、PHPCGIで動作させる場合のセキュリティも含めた留意点が記されているようです。

  • ケース 1: 配布制限がないファイルのみを配布
  • ケース 2: --enable-force-cgi-redirect を使用
  • ケース 3: doc_root または user_dir を設定
  • ケース 4: Webツリーの外にPHPパーサを置く

まずケース1については、サーバ上になんら隠す必要のあるファイルが存在しない場合です。まぁ、あり得ないと考えても良いと思います。
ケース4については、shebang(シェバン)行で

#!/usr/local/bin/php
<?php
print ...

とする手法です。

ケース2とケース3が注意すべき手法、になります。ここで上のPHPマニュアルに再度注目ですが、CGIの処理について「リダイレクト云々」と書かれています。で、びっくりしたのが次の一文。

セットアップ時には、通常、PHP 実行バイ ナリを Web サーバーの cgi-bin ディレクトリにインストールします。

PHP: CGI バイナリとしてインストール - Manual

・・・え?
そ、そんなものなの?
だってPerlCGIとかはそんなの関係ないじゃん・・・ってあれはケース4か。確かにCGIってfork()される別プロセスで環境変数とか標準入出力でやりとりするのだから、昔はC言語とかで普通に組んでいたという話も聞いたことあるので、なるほど確かにそういう場合もあったのだろうなぁとは思うのだけれど。

で。

http://my.host/cgi-bin/php?/etc/passwd
URL において疑問符 (?) の後のクエリー情報は、CGI インターフェー スにより、インタプリタコマンドライン引数として渡されます。

PHP: CGI バイナリとしてインストール - Manual

・・・え?そ、そんなものなの?つまりこれって、

php /etc/passwd

するのと一緒だよね?

さらに。

http://my.host/cgi-bin/php/secret/doc.html
URL の PHP バイナリ名の後のパス情報の部分、つまり/secret/doc.html は、 CGI プログラムによりオープンされて実行される ファイルの名前を指定するために従来より使用されています。

PHP: CGI バイナリとしてインストール - Manual

し、知らなかった・・・。orz
さらにこの下の解説を読むと、Apacheの場合は Action ディレクティブを使うことで、

http://foobar/secret/script.php
→
http://foobar/cgi-bin/php/secret/script.php

というように内部的にリダイレクトしてくれるそうです。しかもこのとき、最初に"/secret/script.php"のアクセス制限をチェックしてくれるそうです。
ただし、最初から

http://foobar/cgi-bin/php/secret/script.php

というURLがリクエストされてしまうと、この「アクセスチェック→内部リダイレクト」がスポイルされてしまうためヤバイそうです。
えーと、つまり

http://foobar/cgi-bin/php/etc/passwd

されたのと一緒、みたいな。

ということで、Apacheの場合、「内部リダイレクトされた」リクエストじゃないと危険で動かせないと言うわけです。で、Apacheの場合は内部リダイレクトされた場合はCGI環境変数としてREDIRECT_STATUS, REDIRECT_URL, REDIRECT_QUERY_STRING にリダイレクト元の情報を入れるそうです。
Tips (CGI, Perl, Unix and etc.)
なので、REDIRECT_STATUS環境変数がある場合のみ(かつApacheの場合のみ)、CGI経由で処理続行OK!と・・・PHPの場合、しているみたいです。

では実際のPHPソースコードを見てみます。手元に、PHP5.2.5のGNU GLOBALSを使ってtag情報をhtmlizeしたのがあったのでそれ使うと見つけるの早かったです。うー、Windowsユーザなんで。

sapi/cgi/cgi_main.c :

#if FORCE_CGI_REDIRECT
  /* check force_cgi after startup, so we have proper output */
  if (cgi && CGIG(force_redirect)) {
    /* Apache will generate REDIRECT_STATUS,
     * Netscape and redirect.so will generate HTTP_REDIRECT_STATUS.
     * redirect.so and installation instructions available from
     * http://www.koehntopp.de/php.
     *   -- kk@netuse.de
     */
    if (!getenv("REDIRECT_STATUS")
      && !getenv ("HTTP_REDIRECT_STATUS")
      /* this is to allow a different env var to be configured
         in case some server does something different than above */
      && (!CGIG(redirect_status_env) || !getenv(CGIG(redirect_status_env)))
      ) {
      SG(sapi_headers).http_response_code = 400;
      PUTS("<b>Security Alert!</b>(途中省略)</p>\n");
      ...
      return FAILURE;
    }
  }
#endif  /* FORCE_CGI_REDIRECT */

まずFORCE_CGI_REDIRECT定義ですが、これはWindowsの場合はデフォルトで定義*1されるようです。autoconfig系は分からないのですが、恐らくconfigure時に --enable-force-cgi-redirect オプションで有効になるのではないでしょうか。
他にもFORCE_CGI_REDIRECTでif - endif してるところがあって、結果だけ言うとそれらは

cgi.force_redirect
cgi.redirect_status_env

を使うように(見るように)コンパイルしてます。逆に言うといくらphp.iniで定義してあっても、--enable-force-cgi-redirectを指定していないとその効果は無い、ということです。

先ほどから「Apacheの場合は」が続いていますが、Apache以外のWebサーバの場合は内部リダイレクトの有無を見分けられない場合もあるようです。php.iniのコメントにあるようにIISもこれに該当するそうです。なので、そうした場合は「やむを得ず」リダイレクト有無はチェックせずに実行してしまおうね、というので、明示的に

cgi.force_redirect = 0

php.iniで指定することで、チェックは行われずCGIの実行が継続されるようです。

もしもApache以外のサーバで、しかもforce_redirectが有効化されていた場合に備えて、

cgi.redirect_status_env

が設定されていれば、チェックはされますが結果として素通しになります。なので、"KNOW WHAT YOU ARE DOING FIRST."となっているようです。

何かまとまらなくなりましたが、こうして考えるとActionなどで拡張子判別でPHPスクリプトを実行させるのは意外と、設定を誤ると脆弱性につながる危険性があるようです。なのでphp.iniにも、「何やってるのか完全に理解してから明示的に設定してね、あと自己責任だから!」とうるさいほど書かれているのでしょう。

正直、CGIで動かす気が萎えた・・・。
結局 Options で ExecCGI を設定し、shebang行で実行するPHPパーサバイナリをきちんと指定する、冒頭のPHPマニュアルで言うケース4のやり方(=結局PerlCGIと同等)が良いのかも知れない・・・。
そうなると、どこかしらフレームワーク的なものを作って、エントリポイントは1ファイルにしないと都合が悪いですよね。エントリポイントが複数有ると、環境で実行バイナリの位置が変わると全部のshebang行直さないといけないから・・・。

はふ〜ん。

*1:main/config.w32.h