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))