web-dev-qa-db-ja.com

ディレクトリが別のディレクトリのサブディレクトリであるかどうかを確認する方法

私はPythonでテンプレートシステムを書くのが好きです。これにより、ファイルを含めることができます。

例えば.

これはテンプレートです
 safe_include`othertemplate.rst` 
でファイルを安全に含めることができます

ご存知のように、ファイルを含めることは危険な場合があります。たとえば、ユーザーが独自のテンプレートを作成できるWebアプリケーションでテンプレートシステムを使用する場合、ユーザーは次のようなことを行う可能性があります

パスワードが必要です: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がFalseである限り、安全だと思います。もちろん、シンボリックリンクを許可すると、ユーザーがそのようなリンクを作成できる場合は安全でなくなります。

PDATE-Solution中間ディレクトリがシンボリックリンクの場合、上記のコードは機能しません。これを防ぐには、realpathの代わりにabspathを使用する必要があります。

PDATE: commonprefix()Reorxが指摘する問題を解決するために、末尾に/をディレクトリに追加します。

これにより、シンボリックリンクが実際の宛先に展開されるため、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
41
Simon

os.path.realpath(path):指定されたファイル名の正規パスを返し、パスで検出されたシンボリックリンクを削除します(オペレーティングシステムでサポートされている場合)。

それをディレクトリとサブディレクトリ名で使用し、後者が前者で始まることを確認します。

11
blaze

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
40
jme

提案された方法の多くに関する問題

文字列比較またはos.path.commonprefixメソッドを使用してディレクトリの親子関係をテストする場合、これらは同様の名前のパスまたは相対パスでエラーが発生しやすくなります。例えば:

  • /path/to/files/myfileは、多くのメソッドを使用して/path/to/fileの子パスとして表示されます。
  • /path/to/files/../../myfilesは、多くのメソッドで/path/myfiles/myfileの親として表示されません。実際、そうです。

Rob Dennisによる 前の回答 は、これらの問題に遭遇することなくパスの親子を比較するための優れた方法を提供します。 Python 3.4は、これらの種類のパス操作をより高度な方法で実行できるpathlibモジュールを追加しました。オプションで、基盤となるOSを参照する必要はありません。jmeは another前の回答 あるパスが別のパスの子であるかどうかを正確に判断するためにpathlibを使用する方法。pathlibを使用したくない場合(理由はわからないが、かなり素晴らしい)次にPython 3.5がos.pathに新しいOSベースのメソッドを導入しました。これにより、はるかに少ないコードで、正確でエラーのない方法でパスの親子チェックを実行できます。 。

Python 3.5の新機能

Python 3.5では、関数os.path.commonpathが導入されました。これは、コードが実行されているOSに固有のメソッドです。次のように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])

正確なワンライナー

Python 3.5。のifステートメントにロット全体を組み合わせることができます。醜いですが、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
17
Tom Bull
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
jgoeders

それで、私はこれを必要としました、そして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))
6
Rob Dennis
def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)
5
Juan A. Navarro

私はpathlibの大ファンなので、別の回答で述べた「other_path.parentsのパス」アプローチが好きですが、そのアプローチは少し重いと感じます(パスのルートへの親ごとに1つのPathインスタンスを作成します)。また、path == other_pathがそのアプローチで失敗するのに対して、os.commonpathはそのケースで成功します。

以下は異なるアプローチであり、さまざまな回答で特定された他の方法と比較した独自の長所と短所があります。

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

これはもう少し冗長ですが、アプリケーションの共通ユーティリティモジュールの関数として簡単に追加できます。また、起動時にメソッドをPathに追加することもできます。

1
Oliver

私は同様の問題のために以下の関数を使用しました:

def is_subdir(p1, p2):
    """returns true if p1 is p2 or its subdirectory"""
    p1, p2 = os.path.realpath(p1), os.path.realpath(p2)
    return p1 == p2 or p1.startswith(p2+os.sep)

シンボリックリンクで問題が発生した後、関数を変更しました。現在は、両方のパスがディレクトリであるかどうかをチェックします。

def is_subdir(p1, p2):
    """check if p1 is p2 or its subdirectory
    :param str p1: subdirectory candidate
    :param str p2: parent directory
    :returns True if p1,p2 are directories and p1 is p2 or its subdirectory"""
    if os.path.isdir(p1) and os.path.isdir(p2):
        p1, p2 = os.path.realpath(p1), os.path.realpath(p2)
        return p1 == p2 or p1.startswith(p2+os.sep)
    else:
        return False
0
Jacek Błocki