How to check whether a directory is a sub directory of another directory

Viewed 53367

I like to write a template system in Python, which allows to include files.

e.g.

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

As you know, including files might be dangerous. For example, if I use the template system in a web application which allows users to create their own templates, they might do something like

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

So therefore, I have to restrict the inclusion of files to files which are for example in a certain subdirectory (e.g. /home/user/templates)

The question is now: How can I check, whether /home/user/templates/includes/inc1.rst is in a subdirectory of /home/user/templates?

Would the following code work and be secure?

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

As long, as allow_symlink is False, it should be secure, I think. Allowing symlinks of course would make it insecure if the user is able to create such links.

UPDATE - Solution The code above does not work, if intermediate directories are symbolic links. To prevent this, you have to use realpath instead of abspath.

UPDATE: adding a trailing / to directory to solve the problem with commonprefix() Reorx pointed out.

This also makes allow_symlink unnecessary as symlinks are expanded to their real destination

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
12 Answers
def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)

New for Python 3.9

pathlib has a new method on PurePath called is_relative_to which performs this function directly. You can read the python documentation on how is_relative_to works, or use this example:

from pathlib import Path

child_path = Path("/path/to/file")
if child_path.is_relative_to("/path"):
    print("/path/to/file is a child of /path") # This prints
if child_path.is_relative_to("/anotherpath"):
    print("/path/to/file is a child of /anotherpath") # This does not print

I used below function for similar problem:

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)

After running into problems with symbolic link I've modified the function. Now it checks if both paths are directories.

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

with your inspirations, this method has been added to my utils:

def is_in_basefolder(path_to_check: PosixPath, basefolder: PosixPath):
        """
        check if a given path is in base folder
        
        parameters:
            path_to_check: a path to match with base folder
            basefolder: the base folder
        """
        path = path_to_check.resolve()
        base = basefolder.resolve()
        
        if path == base:
            return True
        
        if base.stem in path.parts:
            return True
        else:
            return False
Related