Symlink-Safe Directory Walk

Walk a directory without following links that escape the root, recording skipped entries and enforcing a depth limit.

PythonSecurity Boundaries

Code

snippet
import os
from pathlib import Path


def walk_confined(root: str, *, max_depth: int = 32) -> tuple[list[str], list[str]]:
    root_path = Path(root).resolve(strict=True)
    if not root_path.is_dir():
        raise NotADirectoryError(root)
    files: list[str] = []
    skipped: list[str] = []
    stack: list[tuple[Path, int]] = [(root_path, 0)]
    while stack:
        directory, depth = stack.pop()
        if depth >= max_depth:
            skipped.append(str(directory.relative_to(root_path)))
            continue
        with os.scandir(directory) as entries:
            ordered = sorted(entries, key=lambda entry: entry.name)
        for entry in ordered:
            entry_path = Path(entry.path)
            if entry.is_symlink():
                try:
                    target = entry_path.resolve(strict=True)
                except OSError:
                    skipped.append(str(entry_path.relative_to(root_path)))
                    continue
                if target.is_relative_to(root_path) and target.is_file():
                    files.append(str(entry_path.relative_to(root_path)))
                else:
                    skipped.append(str(entry_path.relative_to(root_path)))
            elif entry.is_dir(follow_symlinks=False):
                stack.append((entry_path, depth + 1))
            elif entry.is_file(follow_symlinks=False):
                files.append(str(entry_path.relative_to(root_path)))
    return sorted(files), sorted(skipped)


def main() -> None:
    root = Path(__file__).resolve().parent
    files, skipped = walk_confined(str(root), max_depth=2)
    print(f"files: {len(files)}")
    for name in files[:5]:
        print("  ", Path(name).as_posix())
    print(f"skipped: {len(skipped)}")


if __name__ == "__main__":
    main()