Проверить, является ли каталог подкаталогом другой папки

Я пытаюсь создать функцию, которая будет блокировать доступ к некоторым из моих низкоуровневых каталогов. Например, при создании моего сайта я не хочу разрешать загрузку ниже, чем /var/www/html/site/uploads/, если я допущу ошибку при написании кода. Это также поможет предотвратить опечатку при удалении каталога при удалении файлов кеша или чего-либо еще.

Это легко сделать с помощью realpath() и strcasecmp().

Проблема в том, что я не могу использовать realpath() для создания абсолютного пути, потому что любые вызовы этой функции с несуществующими каталогами вернут FALSE. Ниже моя лучшая попытка посмотреть пути для их проверки.

function is_sub_dir($path = NULL, $parent_folder = NULL) {

    //Convert both to real paths
    //Fails if they both don't exist
    //$path = realpath($path);
    //$parent_folder = realpath($parent_folder);

    //Neither path is valid
    if( !$path OR !$parent_folder ) {
        return FALSE;
    }

    //Standarize the paths
    $path = str_replace('\\', '/', $path);
    $parent_folder = str_replace('\\', '/', $parent_folder);

    //Any evil parent directory requests?
    if(strpos($path, '/..') !== FALSE) {
        return FALSE;
    }

    //If the path is greater
    if( strcasecmp($path, $parent_folder) > 0 ) {
        return $path;
    }

    return FALSE;
}

//BAD FOLDER!!!
var_dump(is_sub_dir('/var/www/html/site/uploads/../', '/var/www/html/site/uploads/'));

Кто-нибудь знает, как правильно установить блоки путей к файлам для защиты от низкоуровневого доступа к папкам?

:ОБНОВЛЕНО:

Далее я хочу пояснить, что этот метод проверки будет использоваться на нескольких серверах, а также при создании каталогов выше заданного каталога.

Например, в моем каталоге загрузок я хочу разрешить администраторам создавать новые подкаталоги, такие как ...uploads/sub/. Выяснив надежный способ гарантировать, что указанный каталог на самом деле выше, чем родительский каталог, я могу чувствовать себя в большей безопасности, позволяя своим администраторам работать с файловой системой в папке загрузки.

Поэтому, поскольку мне может потребоваться убедиться, что uploads/sub выше, чем uploads/, прежде чем я создам его, я не могу использовать realpath(), потому что uploads/sub< /strong> больше не существует.

Что касается фактического местоположения папки загрузки, которая вычисляется PHP на лету.

define('UPLOAD_PATH', realpath(dirname(__FILE__)));

: ОБНОВЛЕНИЕ 2:

У меня есть идея, что если бы я использовал realpath для сравнения всего пути за вычетом последнего сегмента каталога. Тогда, даже если этот последний сегмент каталога все еще необходимо создать, остальную часть пути можно заставить соответствовать минимальному родительскому каталогу?


person Xeoncross    schedule 27.10.2009    source источник
comment
Я бы использовал scandir, он довольно гибкий.   -  person CodeJoust    schedule 27.10.2009


Ответы (5)


Не ответ на ваш непосредственный вопрос, но вы не должны никогда использовать черные списки в реальной среде безопасности.

Это потому, что вы можете изменить макеты каталогов, но забудете обновить черный список, оставив вашу безопасность бессильной.

С помощью белого списка вы перечисляете места, куда им разрешено загружать. Если вы затем измените макет и забудете изменить белый список, ваша безопасность на самом деле повысится, а не понизится. А протестные вопли ваших пользователей предупредят вас о вашей забывчивости.

Что касается запрета доступа к каталогам более низкого уровня, я думаю, что это просто вопрос получения реального пути (со всеми этими ./, ../ и \\, преобразованными в нормализованную форму), а затем, если он начинается с /var/www/html/site/uploads/, потерпеть неудачу, если есть еще / символов после этого.

Я делал эту нормализацию раньше, и она в основном состоит из (по памяти):

  1. Замените все \\ на / (так что он использует UNIX-измы).
  2. Если он не начинается с /, добавьте свой базовый каталог впереди (так что это всегда абсолютный путь).
  3. Замените все /./ на / (избавьтесь от бесполезных перемещений «оставаться в текущем каталоге»).
  4. Замените все /X/../ на /, где X — любой символ, отличный от / (избавляется от перемещения каталога вниз-вверх).
  5. Затем убедитесь, что это правильное местоположение.

То, что осталось, как правило, безопасно для использования, хотя могут быть крайние случаи в зависимости от того, доступно ли больше команд перемещения каталога (я видел ... как эквивалент ../..). Ваш пробег может отличаться.

person paxdiablo    schedule 27.10.2009
comment
Спасибо, я обновил свой вопрос после прочтения этого. Проблема в том, что некоторые из запрошенных папок могут быть недействительными — пока. Кроме того, я не знал о ... ярлыке. - person Xeoncross; 27.10.2009
comment
Когда я говорю действительный, я не имею в виду, что он существует, просто это не плохое место (например, /var/data/../../etc/passwd). Этот метод полностью основан на обработке строк, а не на том, что находится в файловой системе. - person paxdiablo; 28.10.2009

Лучший способ, который я знаю, - это просто сделать все, кроме каталога загрузки, недоступным для записи пользователем, от имени которого работает веб-сервер. Например, в Debian Apache запускается от имени пользователя www-data. Итак, я удостоверяюсь, что все каталоги, которые Apache может обслуживать, доступны для чтения/исполнения всему миру, принадлежат какому-либо пользователю, отличному от www-data, и доступны для записи только этому пользователю (или какой-либо группе администраторов). Затем, если каталог должен быть доступен для записи веб-серверу, я делаю его доступным для записи www-data либо через группу, либо через владельца.

Если это возможно, рекомендуется также переместить доступные для записи каталоги за пределы основного дерева сервера. Если я управляю сервером, я специально для этой цели создаю каталог под /var/lib. Если я не контролирую сервер, я ставлю его рядом с обслуживаемыми каталогами. Например, используя ваш пример /var/www/site, я бы добавил еще один уровень, скажем, /var/www/site/html (для прямого обслуживания HTML и скриптов верхнего уровня), /var/www/site/scripts (для include()ed PHP-скриптов) и /var/www/site/data (для загруженных данных). Конечно, если вам нужно обслуживать загруженные данные, вам нужно либо написать PHP-оболочку для их вывода, либо поместить их под /var/www/site/html/uploads, подобно тому, как вы это делаете сейчас.

person Michael Johnson    schedule 27.10.2009
comment
Да, это самый безопасный метод. Однако на некоторых серверах, на которых это будет развернуто, администраторы смогут создавать новые каталоги в папке загрузок — отсюда и проблема проверки допустимого пути. - person Xeoncross; 27.10.2009
comment
Должна быть возможность использовать этот метод. Если вы установите бит закрепления группы, группа останется неизменной для подкаталогов. Затем, если ваши администраторы являются членами этой группы, они могут создавать подкаталоги, и они по-прежнему жизнеспособны. - person Michael Johnson; 28.10.2009

Следующий код работает, даже если последний каталог в пути еще не существует. Любая нечестная игра или дополнительные отсутствующие каталоги возвращают false.

Ограничение этой функции заключается в том, что она работает только с путями, которые (в основном) уже существуют, и именами каталогов, использующими стандартные английские символы (/.hts/, /files.90.3r3/, /my_photos/) и т. д..)

function is_sub_dir($path = NULL, $parent_folder = SITE_PATH) {

    //Get directory path minus last folder
    $dir = dirname($path);
    $folder = substr($path, strlen($dir));

    //Check the the base dir is valid
    $dir = realpath($dir);

    //Only allow valid filename characters
    $folder = preg_replace('/[^a-z0-9\.\-_]/i', '', $folder);

    //If this is a bad path or a bad end folder name
    if( !$dir OR !$folder OR $folder === '.') {
        return FALSE;
    }

    //Rebuild path
    $path = $dir. DS. $folder;

    //If this path is higher than the parent folder
    if( strcasecmp($path, $parent_folder) > 0 ) {
        return $path;
    }

    return FALSE;
}
person Xeoncross    schedule 27.10.2009
comment
Примечание: DS = DIRECTORY_SEPARATOR - person Xeoncross; 27.10.2009
comment
Вы должны изменить !$dir на $dir === false (то же самое с !$folder), чтобы правильно проверить наличие false. Кроме того, я считаю, что strcasecmp() - это неправильный метод для использования. strpos($path, $parent_folder) › 0, я думаю, правильное сравнение. Вызов в размещенном коде проверяет, является ли $parent_folder лексически большим, чем $path, поэтому strcasecmp(z, a) будет пройден. Явно не то, что вы ищете. Наконец, я бы не стал проводить здесь сравнение без учета регистра, если только вы не работаете исключительно в Windows. В противном случае gooddir/path будет сравниваться с GOODDIR/path, который в Linux будет двумя разными каталогами. - person Michael Johnson; 29.10.2009

Это так просто?

const UPLOAD_DIR = '/var/www/site/uploads/';

Каждый раз, когда вам нужно его использовать, вы просто используете константу, так как кажется, что есть только одно место, куда вы можете загрузить. Таким образом, вы не сможете так легко облажаться.

if ($dir != UPLOAD_DIR) {
   // No access; Error
}

Иногда, чтобы защитить себя от самого себя, нужно просто быть бдительным. Просто не забудьте вызвать is_sub_dir() перед любым доступом к файлу.

ИЗМЕНИТЬ:

Теперь, когда вопрос стал более ясным, я вижу, что мой ответ не имеет смысла. =) Мой единственный другой совет - повторить то, что сказали другие: дезинфицировать, дезинфицировать, дезинфицировать.

person Chris Kloberdanz    schedule 27.10.2009
comment
На самом деле проблема не в том, чтобы проверить, совпадает ли каталог с другим, а в том, чтобы проверить, является ли каталог дочерним по отношению к другому. - person Xeoncross; 27.10.2009
comment
Думаю, я просто подумал, что, поскольку вы разрешаете загрузку только в один каталог, достаточно будет сравнить каталог, в который вы пытаетесь записать, с ожидаемым. Любой подкаталог не будет разрешен, как и любой другой каталог. - person Chris Kloberdanz; 27.10.2009
comment
Что произойдет, если кто-то захочет написать файл с именем ../../../../bin/sh? - person jprete; 27.10.2009
comment
Его вопрос был обновлен, чтобы лучше объяснить себя, поэтому теперь мой комментарий следует игнорировать. - person Chris Kloberdanz; 27.10.2009

Я знаю, что на него уже был дан ответ, но самым простым решением не допускать «..» в путях было бы:

if(realpath(dirname($file)) == dirname($file)){ // OK! }

person Flash Thunder    schedule 08.02.2016
comment
пользователь добавил информацию о несуществующих каталогах позже... не моя вина. - person Flash Thunder; 24.05.2016