HEX
Server: Apache
System: Linux www3.pit.tblive.com 5.14.0-687.38.1.el9_8.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Aug 12 17:19:12 EDT 2026 x86_64
User: awaldron (1020)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: //opt/cloudlinux/venv/lib64/python3.11/site-packages/clcagefslib/webisolation/mount_config.py
#!/opt/cloudlinux/venv/bin/python3 -sbb
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
"""
Mount configuration builder for website isolation.

The code handles all standard behavior (docroot isolation, home overlay, etc).
"""

import os.path
from dataclasses import dataclass, field

from .jail_config import MountEntry


@dataclass
class IsolatedRootConfig:
    """
    Configuration for a directory overlay.

    Closes access to a directory by mounting a fake/empty directory over it,
    then selectively exposing only whitelisted paths.

    Storage is computed as: {storage_base}/{name}
    """

    # Path to the root of this storage (e.g. ~/.clcagefs/website/123/home)
    root_path: str

    # Real directory to close
    target: str

    # Use temporary tmpfs for storage (default: real directory)
    persistent: bool = True

    # List of mounts made inside of this root (dynamically)
    mounts: list[MountEntry] = field(default_factory=list)

    # F-36 (CLOS-5423): defense-in-depth path-traversal guard at the mount
    # sink. All callers reach mount() via write_jail_mounts_config, which
    # runs validate_docroot (regex allowlist rejecting whitespace, `[`,
    # `]`, `,`, `;`, quotes) and validate_docroot_no_symlinks (O_NOFOLLOW
    # component walk from the resolved user home) before this point.
    # However, those checks compare *resolved* paths while mount() below
    # composes a mount-target string from the *raw* spellings via
    # os.path.relpath - so a benign operator alias (e.g. `/home -> /home2`
    # with panel-returned `/home2/user/public_html` against a raw
    # `self.target = /home/user`) would produce a relpath prefixed with
    # `..`, and the emitted `{root_path}/{relative_path}` string would
    # lexically escape root_path. Canonicalise both paths here before
    # composing the relative segment and reject any residual escape.
    # Any new caller must uphold the trust-boundary contract above.
    def mount(self, type_, source, target, opts: tuple = tuple()):
        """Mounts whatever asked into the root of isolated storage"""
        # Resolve both sides so an aliased-but-equivalent target
        # (e.g. `/home2/user/public_html` vs raw `self.target=/home/user`
        # under `/home -> /home2`) produces a clean tail segment instead
        # of a `..`-prefixed escape.
        resolved_self = os.path.realpath(self.target)
        resolved_target = os.path.realpath(target)
        relative_path = os.path.relpath(resolved_target, resolved_self)
        # os.path.relpath emits `..` (or a `../`-prefixed string) when the
        # resolved target does not lie under the resolved overlay root -
        # exactly the case that would produce a mount-target string
        # escaping root_path. Refuse: a well-formed jail cannot contain
        # such an entry.
        if relative_path == ".." or relative_path.startswith("../"):
            raise ValueError(
                "Invalid mount target: resolved target "
                f"{resolved_target!r} escapes overlay root "
                f"{resolved_self!r} (target={target!r}, "
                f"self.target={self.target!r})"
            )
        self.mounts.append(MountEntry(type_, source, f"{self.root_path}/{relative_path}", opts))