理想未来ってなんやねん

娘可愛い。お父さん頑張る。

suPHP + chroot パス変換パッチ

suEXECのPHP版、suPHPではサイト毎に実行ユーザーを変更することが出来ますが、その他の機能としてサイト毎にルートディレクトリを変更できるchroot機能も備えています。


例えば、hogeユーザーのホームディレクトリが/home/hoge/だとして、chrootしたとすると/home/hoge/を / とすることで、それ以外の階層のファイル(/etc/passwd等)にアクセスできないように保護することができます。


ところが、このchrootの機能が使い勝手が悪く、hoge.netというVirtualHostのDocumentRootが/var/www/hoge.net/htdocs/だとして、http://hoge.net/test.phpというURLにリクエストがあれば、以下のようになります。

例)suPHP + chrootの動作
/
   var/
      www/
         hoge.net/          ← suPHPでchrootする位置
            htdocs/            ← ApacheのVirtualHostのDocumentRoot
               test.php        ← /test.phpへのリクエストは、このスクリプトが実行される事を期待する。
               image.png       ← /image.pngへのリクエストは、そのまま期待通り。
            var/
               www/
                  hoge.net/
                     htdocs/            ← chrootしたことによって/var/www/hoge.net/htdocs/はこの位置になる。
                        test.php        ← 実際にはここのスクリプトが実行される。ファイルがない場合は404 Not Foundとなる。

chrootしていない場合は /var/www/hoge.net/htdocs/test.phpというスクリプトファイルが実行されますが、/var/www/hoge.net/にchrootした場合、/var/www/hoge.net/htdocs/test.phpではなく、/var/www/hoge.net/をルートディレクトリとした/www/hoge.net/htdocs/test.phpとなる /var/www/hoge.net/var/www/hoge.net/htdocs/test.phpが実行されます。


この仕様が理解しにくい&冗長な為なのか、suPHPについて記載しているブログ等でもchrootについては触れずに記載しているサイトが多いようです。


chrootした場合でもDocumentRoot下のファイルが実行されるようにパスを変換するパッチを、だいぶ以前に作成して放置したままにしていましたが折角作成したので公開したいと思います。


このパッチを使用すると以下のようなイメージでsuPHP + chrootを利用できます。

例)suPHP + chroot パス変換パッチ
/
   var/
      www/
         hoge.net/          ← suPHPでchrootする位置
            htdocs/            ← ApacheのVirtualHostのDocumentRoot
               test.php        ← /test.phpへのリクエストは、このスクリプトが実行される。
               image.png       ← /image.pngへのリクエストは、そのまま期待通り。

suphp + chroot パス変換パッチ

suphp-0.7.1.patch.gz 直

diff -Nuar suphp-0.7.1/doc/suphp.conf-example suphp-0.7.1-patch/doc/suphp.conf-example
--- suphp-0.7.1/doc/suphp.conf-example	2008-04-01 04:14:47.000000000 +0900
+++ suphp-0.7.1-patch/doc/suphp.conf-example	2011-06-12 00:14:25.000000000 +0900
@@ -14,6 +14,9 @@
 ;Path to chroot() to before executing script
 ;chroot=/mychroot
 
+;Translate the script path when chroot() is used
+;script_path_translation=false
+
 ; Security options
 allow_file_group_writeable=false
 allow_file_others_writeable=false
diff -Nuar suphp-0.7.1/src/Application.cpp suphp-0.7.1-patch/src/Application.cpp
--- suphp-0.7.1/src/Application.cpp	2009-03-15 02:55:25.000000000 +0900
+++ suphp-0.7.1-patch/src/Application.cpp	2011-06-12 01:17:56.000000000 +0900
@@ -66,6 +66,7 @@
         std::string scriptFilename;
         UserInfo targetUser;
         GroupInfo targetGroup;
+	std::string chrootPath;
 
         // If caller is super-user, print info message and exit
         if (api.getRealProcessUser().isSuperUser()) {
@@ -104,7 +105,7 @@
         // so do this before changing process permissions
         if (config.getChrootPath().length() > 0) {
             PathMatcher pathMatcher = PathMatcher(targetUser, targetGroup);
-            std::string chrootPath = pathMatcher.resolveVariables(config.getChrootPath());
+            chrootPath = pathMatcher.resolveVariables(config.getChrootPath());
             api.chroot(chrootPath);
         }
 
@@ -116,6 +117,17 @@
         // Prepare environment for new process
         newEnv = this->prepareEnvironment(env, config, targetMode);
 
+	if (chrootPath.length() > 0 && config.getScriptPathTranslation())
+	{
+            std::string documentRoot;
+            scriptFilename.replace(1, chrootPath.length(), "");
+            newEnv.setVar("SCRIPT_FILENAME", scriptFilename);
+
+            documentRoot = env.getVar("DOCUMENT_ROOT");
+            documentRoot.replace(1, chrootPath.length(), "");
+            newEnv.setVar("DOCUMENT_ROOT", documentRoot);
+	}
+
         // Set PATH_TRANSLATED to SCRIPT_FILENAME, otherwise
         // the PHP interpreter will not be able to find the script
         if (targetMode == TARGETMODE_PHP && newEnv.hasVar("PATH_TRANSLATED")) {
diff -Nuar suphp-0.7.1/src/Configuration.cpp suphp-0.7.1-patch/src/Configuration.cpp
--- suphp-0.7.1/src/Configuration.cpp	2008-03-29 22:02:36.000000000 +0900
+++ suphp-0.7.1-patch/src/Configuration.cpp	2011-06-12 00:11:20.000000000 +0900
@@ -112,6 +112,11 @@
 #endif
     this->umask = 0077;
     this->chroot_path = "";
+#ifdef OPT_ENABLE_SCRIPT_PATH_TRANSLATION
+    this->script_path_translation = true;
+#else
+    this->script_path_translation = false;
+#endif
 }
 
 void suPHP::Configuration::readFromFile(File& file) 
@@ -157,6 +162,8 @@
                 this->umask = Util::octalStrToInt(value);
             else if (key == "chroot")
                 this->chroot_path = value;
+            else if (key == "script_path_translation")
+                this->script_path_translation = this->strToBool(value);
             else 
                 throw ParsingException("Unknown option \"" + key + 
                                        "\" in section [global]", 
@@ -250,3 +257,7 @@
 std::string suPHP::Configuration::getChrootPath() const {
     return this->chroot_path;
 }
+
+bool suPHP::Configuration::getScriptPathTranslation() const {
+    return this->script_path_translation;
+}
diff -Nuar suphp-0.7.1/src/Configuration.hpp suphp-0.7.1-patch/src/Configuration.hpp
--- suphp-0.7.1/src/Configuration.hpp	2008-03-29 22:02:36.000000000 +0900
+++ suphp-0.7.1-patch/src/Configuration.hpp	2011-06-11 23:59:14.000000000 +0900
@@ -58,6 +58,7 @@
         int min_gid;
         int umask;
         std::string chroot_path;
+        bool script_path_translation;
 
         /**
          * Converts string to bool
@@ -166,6 +167,11 @@
          * Return chroot path
          */
         std::string getChrootPath() const;
+
+        /**
+         * Returns whether translate the script path when chroot is used
+         */
+        bool getScriptPathTranslation() const;
     };
 };
 
diff -Nuar suphp-0.7.1/src/config.h.in suphp-0.7.1-patch/src/config.h.in
--- suphp-0.7.1/src/config.h.in	2009-03-15 03:07:57.000000000 +0900
+++ suphp-0.7.1-patch/src/config.h.in	2011-06-13 03:23:40.000000000 +0900
@@ -40,6 +40,10 @@
    DOCUMENT_ROOT */
 #undef OPT_DISABLE_CHECKPATH
 
+/* Define if you want to enable translate the script path when 
+   chroot is used */
+#undef OPT_ENABLE_SCRIPT_PATH_TRANSLATION
+
 /* Defines path to logfile */
 #undef OPT_LOGFILE

インストール

CentOS 5.xでの使用例を記載します。


まず、httpd-devel apr-develが必要なのでインストールされていない場合はインストールを行っておいて下さい。

yum install httpd-devel apr-devel


suPHPをパッチを当ててインストールします。

cd /var/tmp
wget http://www.suphp.org/download/suphp-0.7.1.tar.gz
wget http://d.hatena.ne.jp/pcmaster/files/suphp-0.7.1.patch.gz?d=y -O suphp-0.7.1.patch.gz
tar xvzf suphp-0.7.1.tar.gz
cd suphp-0.7.1
zcat ../suphp-0.7.1.patch.gz|patch -p1
./configure \
     --prefix=/usr \
     --sysconfdir=/etc \
     --with-apr=/usr/bin/apr-1-config \
     --with-apxs=/usr/sbin/apxs \
     --with-apache-user=apache \
     --with-setid-mode=paranoid \
     --with-php=/usr/bin/php-cgi \
     --with-logfile=/var/log/httpd/suphp_log \
     --enable-SUPHP_USE_USERGROUP=yes \
     --with-min-uid=100 \
     --with-min-gid=100
make
sudo make install
sudo sh -c "echo 'LoadModule suphp_module modules/mod_suphp.so' > /etc/httpd/conf.d/suphp.conf"
sudo cp doc/suphp.conf-example /etc/suphp.conf
sudo vim /etc/suphp.conf
sudo /etc/init.d/httpd restart

suPHP + chrootの準備

chrootphpを実行させるための環境を準備します。


php-cgiを実行するのに必要なライブラリをコピーします。
まず下記の内容を、mkphpjail.shとして保存します。

#!/bin/bash

CHROOTDIR=$1

if [ -z "${CHROOTDIR}" ]
then
    echo "Usage: ${0##*/} CHROOTDIR"
    exit 1
elif [ ! -d "${CHROOTDIR}" ]
then
    echo "${CHROOTDIR}: Directory not found. "
    echo "please mkdir ${CHROOTDIR};"
    exit 1
fi

mkdir -p ${CHROOTDIR}/{etc/php.d,htdocs,bin,usr/{bin,local/bin,lib64/php,share/{pear,zoneinfo}}}
#cp -f /bin/bash ${CHROOTDIR}/bin/
cp -fp /etc/php.ini ${CHROOTDIR}/etc/
cp -fp /etc/php.d/* ${CHROOTDIR}/etc/php.d/
cp -fp /usr/bin/php-cgi ${CHROOTDIR}/usr/bin/
cp -Rfp /usr/share/zoneinfo/* ${CHROOTDIR}/usr/share/zoneinfo/
cp -Rfp /usr/share/pear/* ${CHROOTDIR}/usr/share/pear/
cp -Rfp /usr/lib64/php/* ${CHROOTDIR}/usr/lib64/php/
ldd ${CHROOTDIR}/{bin,usr/bin,usr/local/bin,usr/lib64/php/modules}/*|grep '/.*(0x'|sed -e 's,\s*\([^/]*=> \)\?/\(.*\) (0x.*,\2,'|sort|uniq|while read lib; 
do
  LIBDIR="${CHROOTDIR}/`dirname ${lib}`";
  test ! -d ${LIBDIR} && mkdir -p ${LIBDIR};
  cp -fpv /${lib} ${CHROOTDIR}/${lib};
done


mkphpjail.shを実行します。

$ chmod +x mkphpjail.sh
$ mkdir -p /var/www/hoge.net
$ ./mkphpjail.sh /var/www/hoge.net


php-cgiをjail環境で実行するのに必要なモジュールが、/var/www/hoge.net以下にコピーされます。
私の環境の場合、サイズは43MB程でした。
複数サイトを生成する場合でオーバーヘッドが気になる場合は、ハードリンク(rsyncで--link-destでコピーする等)にしてしまうのが良いかもしれません。


次にユーザーを追加します。

$ sudo useradd hogenet_user -d /var/www/hoge.net


次に、/etc/suphp.confを編集します。
chrootを設定しscript_path_translation=trueとすることでパス変換の機能が有効になります。

[global]
;Path to logfile
logfile=/var/log/suphp.log

;Loglevel
loglevel=info

;User Apache is running as
webserver_user=apache

;Path all scripts have to be in
docroot=${HOME}/htdocs

;Path to chroot() to before executing script
chroot=${HOME}

;Translate the script path when chroot() is used
script_path_translation=true

; Security options
allow_file_group_writeable=false
allow_file_others_writeable=false
allow_directory_group_writeable=false
allow_directory_others_writeable=false

;Check wheter script is within DOCUMENT_ROOT
check_vhost_docroot=true

;Send minor error messages to browser
errors_to_browser=false

;PATH environment variable
env_path=/bin:/usr/bin

;Umask to set, specify in octal notation
umask=0077

; Minimum UID
min_uid=100

; Minimum GID
min_gid=100


[handlers]
;Handler for php-scripts
application/x-httpd-suphp="php:/usr/bin/php-cgi"

;Handler for CGI-scripts
x-suphp-cgi="execute:!self"

Apacheの設定を行います。suPHP_UserGroupで実行するユーザーおよびグループを設定します。

<VirtualHost *:80>
    DocumentRoot /var/www/hoge.net/htdocs/
    ServerName hoge.net
    ErrorLog logs/hogenet-error_log
    AccessLog logs/hogenet-access_log common 
    suPHP_Engine on
    AddHandler application/x-httpd-suphp .php
    suPHP_AddHandler application/x-httpd-suphp

    suPHP_UserGroup hogenet_user hogenet_user
    suPHP_ConfigPath /etc/

    <Directory "/var/www/hoge.net/htdocs/">
        Options -Indexes Includes MultiViews FollowSymLinks
        AllowOverride All

        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

変更を反映させます。

$ sudo /etc/init.d/httpd restart
||


**確認
テストスクリプトを用意します。

>|bash|
$ echo '<?php phpinfo(); ?>' > /var/www/hoge.net/htdocs/test.php
$ sudo chown hogenet_user:hogenet_user /var/www/hoge.net/htdocs/test.php


ブラウザでアクセスし、phpinfoが表示され、DOCUMENT_ROOTが/htdocs/と変わっていれば正常に動作しています。
suEXECの動作により上記でオーナーを変更しないと実行できないはずですので、そちらも確認してみた方が良いでしょう。


以上、何かのお役に立てる事があれば幸いです。