haxorqt
Home
Console
Upload
information
Create File
Create Folder
About
:
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
clcagefslib
/
webisolation
/
Filename :
mount_config.py
back
Copy
#!/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))