Как проверить, является ли каталог подкаталогом другого каталога - PullRequest
34 голосов
/ 28 сентября 2010

Мне нравится писать систему шаблонов на Python, которая позволяет включать файлы.

, например

    This is a template
    You can safely include files with safe_include`othertemplate.rst`

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

I want your passwords: safe_include`/etc/password`

Поэтому я должен ограничить включение файлов в файлы, которые находятся, например, в определенном подкаталоге (например, /home/user/templates)

Вопрос теперь: как я могу проверить, находится ли /home/user/templates/includes/inc1.rst в подкаталоге /home/user/templates?

Будет ли следующий код работать и быть безопасным?

import os.path

def in_directory(file, directory, allow_symlink = False):
    #make both absolute    
    directory = os.path.abspath(directory)
    file = os.path.abspath(file)

    #check whether file is a symbolic link, if yes, return false if they are not allowed
    if not allow_symlink and os.path.islink(file):
        return False

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

Пока allow_symlink Неверно, я думаю, это должно быть безопасно. Разумеется, использование символических ссылок может сделать небезопасным, если пользователь сможет создавать такие ссылки.

ОБНОВЛЕНИЕ - Решение Приведенный выше код не работает, если промежуточные каталоги являются символическими ссылками. Чтобы предотвратить это, вы должны использовать realpath вместо abspath.

ОБНОВЛЕНИЕ: добавление конечного / в каталог для решения проблемы с commonprefix (), на который указал Реоркс.

Это также делает allow_symlink ненужным, так как символические ссылки расширяются до их реального назначения

import os.path

def in_directory(file, directory):
    #make both absolute    
    directory = os.path.join(os.path.realpath(directory), '')
    file = os.path.realpath(file)

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

Ответы [ 9 ]

29 голосов
/ 12 декабря 2015

Модуль Python 3 pathlib делает это простым благодаря атрибуту Path.parents . Например:

from pathlib import Path

root = Path('/path/to/root')
child = root / 'some' / 'child' / 'dir'
other = Path('/some/other/path')

Тогда:

>>> root in child.parents
True
>>> other in child.parents
False
13 голосов
/ 08 августа 2013
def is_subdir(path, directory):
    path = os.path.realpath(path)
    directory = os.path.realpath(directory)
    relative = os.path.relpath(path, directory)
    return not relative.startswith(os.pardir + os.sep)
12 голосов
/ 08 мая 2016

Проблемы со многими из предложенных методов

Если вы собираетесь проверять происхождение каталога с помощью сравнения строк или os.path.commonprefix методов, они подвержены ошибкам с путями с одинаковыми именами или относительными путями.Например:

  • /path/to/files/myfile будет отображаться как дочерний путь /path/to/file с использованием многих методов.
  • /path/to/files/../../myfiles не будет отображаться как родительский элемент /path/myfiles/myfile многими способами.На самом деле это так.

Предыдущий ответ Роба Денниса дает хороший способ сравнить происхождение пути, не сталкиваясь с этими проблемами.В Python 3.4 добавлен модуль pathlib, который может выполнять такие операции с путями более изощренным способом, необязательно без ссылки на базовую ОС.jme описал в другой предыдущий ответ , как использовать pathlib с целью точного определения, является ли один путь дочерним по отношению к другому.Если вы предпочитаете не использовать pathlib (не уверен, почему, это довольно здорово), тогда Python 3.5 представил новый * основанный на ОС метод в os.path, который позволяет вам выполнять проверку пути родительского и дочернего процесса в такой же точной и ошибочной проверке.свободный способ с намного меньшим количеством кода.

Новое для Python 3.5

В Python 3.5 появилась функция os.path.commonpath.Это метод, специфичный для ОС, в которой выполняется код.Вы можете использовать commonpath следующим образом, чтобы точно определить происхождение пути:

def path_is_parent(parent_path, child_path):
    # Smooth out relative path names, note: if you are concerned about symbolic links, you should use os.path.realpath too
    parent_path = os.path.abspath(parent_path)
    child_path = os.path.abspath(child_path)

    # Compare the common path of the parent and child path with the common path of just the parent path. Using the commonpath method on just the parent path will regularise the path name in the same way as the comparison that deals with both paths, removing any trailing path separator
    return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])

Точный однострочный

Вы можете объединить весь лот в однострочный оператор if в Python3.5.Это уродливо, оно включает в себя ненужные повторяющиеся вызовы на os.path.abspath, и оно определенно не будет соответствовать рекомендациям PEP 8 по длине строки в 79 символов, но если вам нравятся такие вещи, вот так:

if os.path.commonpath([os.path.abspath(parent_path_to_test)]) == os.path.commonpath([os.path.abspath(parent_path_to_test), os.path.abspath(child_path_to_test)]):
    # Yes, the child path is under the parent path
10 голосов
/ 28 сентября 2010

os.path.realpath (путь): возвращает канонический путь указанного имени файла, исключая любые символические ссылки, встречающиеся в пути (если они поддерживаются операционной системой).

Использовать его в каталогеи имя подкаталога, затем проверьте, что последний начинается с первого.

6 голосов
/ 13 июля 2013

Итак, мне это нужно, и из-за критики в отношении commonprefx я пошел другим путем:

def os_path_split_asunder(path, debug=False):
    """
    http://stackoverflow.com/a/4580931/171094
    """
    parts = []
    while True:
        newpath, tail = os.path.split(path)
        if debug: print repr(path), (newpath, tail)
        if newpath == path:
            assert not tail
            if path: parts.append(path)
            break
        parts.append(tail)
        path = newpath
    parts.reverse()
    return parts


def is_subdirectory(potential_subdirectory, expected_parent_directory):
    """
    Is the first argument a sub-directory of the second argument?

    :param potential_subdirectory:
    :param expected_parent_directory:
    :return: True if the potential_subdirectory is a child of the expected parent directory

    >>> is_subdirectory('/var/test2', '/var/test')
    False
    >>> is_subdirectory('/var/test', '/var/test2')
    False
    >>> is_subdirectory('var/test2', 'var/test')
    False
    >>> is_subdirectory('var/test', 'var/test2')
    False
    >>> is_subdirectory('/var/test/sub', '/var/test')
    True
    >>> is_subdirectory('/var/test', '/var/test/sub')
    False
    >>> is_subdirectory('var/test/sub', 'var/test')
    True
    >>> is_subdirectory('var/test', 'var/test')
    True
    >>> is_subdirectory('var/test', 'var/test/fake_sub/..')
    True
    >>> is_subdirectory('var/test/sub/sub2/sub3/../..', 'var/test')
    True
    >>> is_subdirectory('var/test/sub', 'var/test/fake_sub/..')
    True
    >>> is_subdirectory('var/test', 'var/test/sub')
    False
    """

    def _get_normalized_parts(path):
        return os_path_split_asunder(os.path.realpath(os.path.abspath(os.path.normpath(path))))

    # make absolute and handle symbolic links, split into components
    sub_parts = _get_normalized_parts(potential_subdirectory)
    parent_parts = _get_normalized_parts(expected_parent_directory)

    if len(parent_parts) > len(sub_parts):
        # a parent directory never has more path segments than its child
        return False

    # we expect the zip to end with the short path, which we know to be the parent
    return all(part1==part2 for part1, part2 in zip(sub_parts, parent_parts))
4 голосов
/ 17 ноября 2017
def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)
2 голосов
/ 05 сентября 2017

Мне нравится подход "path in other_path.parents", упомянутый в другом ответе, потому что я большой поклонник pathlib, НО я чувствую, что подход немного тяжел (он создает один экземпляр Path для каждого родителя для корня пути ). Также случай, когда path == other_path потерпит неудачу с этим подходом, тогда как os.commonpath будет успешным в этом случае.

Ниже приведен другой подход с собственным набором плюсов и минусов по сравнению с другими методами, определенными в различных ответах:

try:
   other_path.relative_to(path)
except ValueError:
   ...no common path...
else:
   ...common path...

, который является немного более подробным, но может быть легко добавлен как функция в модуле общих утилит вашего приложения или даже добавить метод в Path во время запуска.

0 голосов
/ 06 декабря 2015

На основе другого ответа здесь, с исправлением и с более удобным для пользователя именем:

def isA_subdirOfB_orAisB(A, B):
    """It is assumed that A is a directory."""
    relative = os.path.relpath(os.path.realpath(A), 
                               os.path.realpath(B))
    return not (relative == os.pardir
            or  relative.startswith(os.pardir + os.sep))
0 голосов
/ 10 апреля 2013

Я бы проверил результат от commonprefix с именем файла, чтобы получить лучший ответ, примерно так:

def is_in_folder(filename, folder='/tmp/'):
    # normalize both parameters
    fn = os.path.normpath(filename)
    fd = os.path.normpath(folder)

    # get common prefix
    commonprefix = os.path.commonprefix([fn, fd])
    if commonprefix == fd:
        # in case they have common prefix, check more:
        sufix_part = fn.replace(fd, '')
        sufix_part = sufix_part.lstrip('/')
        new_file_name = os.path.join(fd, sufix_part)
        if new_file_name == fn:
            return True
        pass
    # for all other, it's False
    return False
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...