Внимание! Статья утратила свою актуальность. Смотрите комментарии.
Есть широко распространённое мнение, что реализовать поддержку open_basedir на веб-сервере с nginx и PHP-FPM невозможно. 99% сайтов предлагают следующее решение: для каждого пользователя хостинга заводить свой пул рабочих процессов PHP. Это чрезвычайно неэффективно.
В процессе администрирования веб-сервера в рамках моего собственного онлайн-проекта мной совместно с моим товарищем shade было найдено решение этой проблемы, позволяющее эффективно использовать open_basedir в такой связке. Что значит эффективно? Это значит, что у всех запущенных рабочих процессов будет возможность обслуживать всех пользователей.
Решение это предельно просто и, как мне кажется, к нему никто не приходит по одной простой причине — в силу чрезмерной наводнённости интернета слухами о невозможности решения этой задачи. Утверждение о невозможности порой можно встретить в духе «даже не надейтесь», «даже не пытайтесь» и так далее.
Суть решения состоит в том, чтобы FastCGI-серверу передавать на обработку не запрашиваемый файл, а некоторый промежуточный файл, который потом самостоятельно обратится к необходимому. Это невозможно сделать при помощи auto_prepend_file в конфигурации PHP, но это не составит труда сделать средствами nginx.
fastcgi_param SCRIPT_FILENAME /path/to/your/security.php; fastcgi_param USER_SCRIPT_FILENAME $document_root/$fastcgi_script_name;
Содержимое файла security.php может включать любые инструкции, в том числе и задание требуемых параметров безопасности. Главное — не забыть о том, что перед передачей управления пользовательскому сценарию нужно запретить модификацию этих параметров.
<?php
$users = array();
$users['user1'] = array('site1.net.ru', 'site2.net.ru', 'site3.net.ru');
$users['user2'] = array('site4.co.cc');
$users['user3'] = array('site5.web.id', 'site6.co.cc');
foreach($users as $username=>$vhosts) {
foreach($vhosts as $hostname) {
if($hostname == $_SERVER['SERVER_NAME']) {
ini_set('open_basedir', '/home/' . $username . ':/tmp:/opt/nginx/html/errors');
break 2;
}
}
}
// см. комментарий ниже
//ini_set('disable_functions', 'ini_set,system,exec,passthru,popen,shell_exec,proc_open');
unset($users);
if(!file_exists($_ENV['USER_SCRIPT_FILENAME'])) {
header('HTTP/1.1 404 Not Found');
readfile('/opt/nginx/html/errors/404.html');
} elseif(!is_readable($_ENV['USER_SCRIPT_FILENAME'])) {
header('HTTP/1.1 403 Forbidden');
readfile('/opt/nginx/html/errors/403.html');
} else {
chdir(dirname($_ENV['USER_SCRIPT_FILENAME']));
require($_ENV['USER_SCRIPT_FILENAME']);
}
?>
Разумеется, ничто не мешает вам реализовать сопоставление пользователей и виртуальных узлов так, как вам это нужно. Например, хранить их в отдельном XML-файле или даже сканировать ФС при каждом обращении (если нагрузка не очень высокая).
По материалам собственных публикаций на форуме Шаманграда.

Кстати, нужно сказать, что disable_functions всё-таки не срабатывает. Тем не менее, переопределить open_basedir при помощи ini_set в пользовательском коде уже не удаётся… Странное поведение. Скорее всего, ini_set для одной и той же конфигурационной директивы может быть выполнен только единожды, что, однако, не отражено в документации: http://ru.php.net/ini_set
По памяти: не всякую переменную из php.ini можно (пере)определить, скажем, в апачевском конфиге vhost’а через php_admin_value. Здесь должно быть то же самое.
Немножко поиска…
…For example, some settings may be set within a PHP script using ini_set(), whereas others may require php.ini or httpd.conf.
disable_functions: php.ini only
В общем, это ограничение php.
Ничего особо страшного… но фишка php-fpm c запуском от имени разных пользователей и с разными php.ini по-прежнему вне конкуренции.
У каждого подхода свой недостаток. Конкуренция у Вашего способа серьёзная, хотя бы потому что nginx в паре с php-fpm чаще всего ставят как легковесную альтернативу Apache на слабых VPS, а на слабых VPS остро стоит проблема использования ОЗУ. Если делать на каждого пользователя свой пул воркеров, ресурсы такой системы будут быстро исчерпаны.
По крайней мере, open_basedir задаётся успешно. Возможно, именно по той причине, что он не определён ни в php.ini, ни в php-fpm.xml.
location ~* \.php$
{
fastcgi_pass unix:/tmp/fastcgi.socket;
fastcgi_index index.php;
fastcgi_param DOCUMENT_ROOT /hc45.ru;
fastcgi_param SCRIPT_FILENAME /securityhandler.php;
fastcgi_param USER_SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include /usr/local/etc/nginx/fastcgi_params;
Не подскажете куда копнуть ?
FastCGI sent in stderr: «PHP Notice: Undefined index: USER_SCRIPT_FILENAME in /securityhandler.php on line 19″
php.ini, как оказалось, успешно справляется вот с таким:
[HOST=hostname]
open_basedir = somedir
мне кажется, элегантнее будет.
я не могу понять если вы используете
ini_set(‘open_basedir’, ‘/home/’ . $username . ‘:/tmp:/opt/nginx/html/errors’);
значит в скрипте МОЖНО поставить open_basedir? Если мне надо для одного сайта сделать, я могу просто в php.ini ее выставить, это будет работать, или мне надо вверху каждого скрипта сайта прописывать
ini_set(‘open_basedir’, ‘/home/’)
test, ini_set для параметра open_basedir прокатывает только один раз, то есть если задать его в первом обрабатываемом скрипте, то в скрипте пользователя изменить это значение уже не получится. Мне не известно, является ли это стандартным поведением, но я просто пользуюсь этим и делюсь.
Артём, выглядит действительно много элегантнее, попробую — отпишусь. Особенно интересно по той причине, что я с товарищами в скором будущем заведу коммерческий хостинг с nginx.
P.S: недавно публиковал небольшое обновление по ссылке на источник
Ну что там, в php.ini корректно у вас все работает, затестируйте уже, тут весь рунет ждет
test, да, работает. Как давно ввели эту штуку? Имеет смысл воспользоваться ею…
P.S.: рунет бы лучше сам затестил вместо того, чтобы ждать лентяя
Добавлю немного. Если ставить байсдир в php.ini это распространяется в том числе и на скрипты, которые запускаются с командной строки, то есть вы будете везде приввязаны к одному байс диру. Как вариант можно просто во всех своих скриптах инклудить один скрипт, который будет
ini_set(‘open_basedir’ …
ставить.
test, речь об указании open_basedir в специальной секции вида [HOST=…]. Опции в этой секции, по всей видимости, применяются только к запросам в рамках данного хоста. Что такое хост определить для командной строки затруднительно, поэтому вряд ли эти секции влияют на работу скриптов для командной строки. По крайней мере, мои скрипты (обновляющие RRD-базы итп) остались работоспособными после того как я добавил одну такую секцию…