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
import os.path
from dataclasses import dataclass, field
from .docroot_validation import validate_docroot_no_symlinks
from .jail_config import MountEntry
def _source_looks_tenant_writable(source: str, raw_prefix: str,
resolved_prefix: str) -> bool:
"""Return True if the raw ``source`` string sits lexically under
the tenant's home (as raw or resolved) and therefore counts as a
tenant-writable bind source that must be revalidated at the mount
sink.
The classification is deliberately lexical, not realpath-based:
a symlink swap that makes ``realpath(source)`` escape the home
tree is exactly the case the sink revalidation must catch, so
the classifier cannot itself dereference the symlink or the
swapped-out source would be misclassified as operator-controlled
and skipped. The subsequent validator run performs the safe
fd-walked resolution.
"""
for prefix in (resolved_prefix, raw_prefix):
marker = prefix.rstrip("/") + "/"
if source == prefix or source.startswith(marker):
return True
return False
@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)
# F-16 (CLOS-5954) DiD: mount() only canonicalises the target
# here; the caller-supplied `source` is otherwise stored raw in
# MountEntry and later handed to a root-run bind mounter that
# follows symlinks. If the source is a path under the tenant's
# home (i.e. under self.target), revalidate it with the
# component-walking no-symlink check right at this sink so a
# tenant swap between config-gen and the final persist is
# rejected. Sources rooted outside the tenant's home
# (`/etc/...`, `/var/...`, `/proc/...`, etc.) are operator-
# controlled and are not revalidated - the operator boundary
# is trusted. Full TOCTOU closure still requires the C-side
# mount consumer to bind through an fd (see F-11 / F-14 / F-15).
if _source_looks_tenant_writable(source, self.target, resolved_self):
try:
validate_docroot_no_symlinks(source, self.target)
except ValueError as exc:
# Re-raise with a class-neutral generic message; the
# inner detail is preserved via __cause__ for logs but
# never surfaces to callers.
raise ValueError("Invalid bind source in jail configuration") from exc
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))