haxorqt
Home
Console
Upload
information
Create File
Create Folder
About
:
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
clcagefslib
/
webisolation
/
Filename :
jail_utils.py
back
Copy
# -*- 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 # import os import pathlib import secrets import shutil from pathlib import Path from clcommon import ClPwd from clcommon.clpwd import drop_privileges from clcagefslib.fs import get_user_var_cagefs_path from clcagefslib.io import apply_metadata_nofollow, write_via_tmp # FNV-1a 64-bit hash constants (must match jail C implementation) _FNV_OFFSET_BASIS = 14695981039346656037 _FNV_PRIME = 1099511628211 def get_jail_config_path(user): cagefs_dir = get_user_var_cagefs_path(user) return pathlib.Path(f"{cagefs_dir}/.cagefs/isolates.mounts") def get_website_id(document_root: str): """ Generates unique id for an isolate website using FNV-1a 64-bit hash. FNV-1a has excellent avalanche properties and distribution. Must match the docroot_hash() function in jail C code. """ # CLOS-4642: normalize trailing slash so registration and the lsphp # lookup agree. LiteSpeed's $DOC_ROOT vhost variable (used to feed # per-domain DedicatePhpHandler pools) carries a trailing slash, while # the canonical docroot from cpapi.docroot() does not. document_root = document_root.rstrip("/") or "/" hash_value = _FNV_OFFSET_BASIS for char in document_root.encode("utf-8"): hash_value ^= char hash_value = (hash_value * _FNV_PRIME) & 0xFFFFFFFFFFFFFFFF # Keep 64-bit return f"{hash_value:016x}" def create_website_token_directory(user: str, document_root: str): """ Create website token directory structure and files in /var/cagefs. Creates: - /var/cagefs/<user>/.cagefs/website/<website_id>/ - token directory """ pw = ClPwd().get_pw_by_name(user) website_id = get_website_id(document_root) # Token directory in /var/cagefs website_base_dir = Path(get_user_var_cagefs_path(user)) / ".cagefs/website" website_dir = website_base_dir / website_id # mode to prevent directory listing by other users website_base_dir.mkdir(exist_ok=True, parents=True, mode=0o751) # mode to allow user list /var/.cagefs when it's mounted for website website_dir.mkdir(exist_ok=True, mode=0o755) # Force-correct ownership and mode in case the directory was created # earlier by the per-domain ns-cache code with a different uid/gid context # (e.g. as root:nobody from an Apache-spawned lsphp request that hit the # jail before this site's isolation was enabled). mkdir(exist_ok=True) # is a no-op for an existing directory and would not heal those perms. # Use the O_NOFOLLOW helper so a symlink swapped in at <website_dir> # between mkdir and chmod/chown raises ELOOP rather than being followed. apply_metadata_nofollow(str(website_dir), 0o755, 0, 0) token = _generate_password(32) # create token file read-only for owner, others cannot read it # this replicates behavior of regular token # note: user can still use chown to change it # token with NON 400 mode would be rejected by cagefs.server.c:check_tokenfile_perms() token_file_path = f"{website_dir}/.cagefs.token" write_via_tmp(website_dir, token_file_path, token) apply_metadata_nofollow(token_file_path, 0o400, pw.pw_uid, 0) # create file with document root path # that we can use as trusted source in proxyexec docroot_file_path = f"{website_dir}/.cagefs.website" write_via_tmp(website_dir, docroot_file_path, document_root) # only read permissions, without modification # we use this marker as a trusted source of document root # it should not be modifiable by user in any way apply_metadata_nofollow(docroot_file_path, 0o444, 0, 0) def _mkdir_nofollow_under(parent_fd: int, name: str, mode: int) -> int: """Create or open ``name`` under ``parent_fd`` rejecting any symlink. mkdirat(name, parent_fd) followed by openat(name, parent_fd, O_NOFOLLOW | O_DIRECTORY) — the open raises ELOOP if the path was pre-planted as a symlink and ENOTDIR if it is a non-directory inode. EEXIST on the mkdir is benign (idempotent re-run); other errors propagate. Returns an fd opened on the real directory inode that the caller is responsible for closing. """ try: os.mkdir(name, mode=mode, dir_fd=parent_fd) except FileExistsError: pass return os.open( name, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY | os.O_CLOEXEC, dir_fd=parent_fd, ) def create_overlay_storage_directory(user: str, document_root: str): """ Create overlay storage directory in user's home. Creates: - <homedir>/.cagefs/websites/<website_id>/ - storage base for overlays Drops privileges to user before creating to ensure proper ownership. """ pw = ClPwd().get_pw_by_name(user) if not Path(pw.pw_dir).exists(): return # Walk the path component-by-component with O_NOFOLLOW on every # openat. Path.mkdir(parents=True, exist_ok=True) is a no-op when a # symlink already sits on the path, so the same path string would # later be embedded verbatim as a bind-mount source in # isolates.mounts and dereferenced by the root-run jail consumer — # letting the tenant pre-aim the storage path away from their home. # Rejecting symlinks on each component (including .cagefs and # .cagefs/websites, which both live in tenant-writable space) # closes that. Done under drop_privileges so the mkdirs land with # the tenant's uid/gid; the resolved fds are opened against the # real inode, not via path resolution after the fact. home_fd = os.open(pw.pw_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC) cagefs_fd = None websites_fd = None leaf_fd = None try: # Ensure ``.cagefs`` exists as the tenant so CageFS proper's own # tenant-side flows keep working (see the .cagefs-must-stay- # user-owned note below). ``.cagefs/websites`` also starts here # tenant-owned on the very first call; after the seal below it # becomes root:root 0755 and subsequent calls open it as root # (traversal permitted for other via 0755). with drop_privileges(user): cagefs_fd = _mkdir_nofollow_under(home_fd, ".cagefs", 0o750) websites_fd = _mkdir_nofollow_under(cagefs_fd, "websites", 0o750) # Post-review Bugbot HIGH on !215: create the per-website leaf # as ROOT (outside drop_privileges). Once ``websites`` is sealed # root:root 0755, the tenant cannot mkdir a new leaf under it — # a second-site provisioning would otherwise fail with EACCES # and multi-site isolation would break. Creating as root works # on both the first call (websites still tenant-owned 0o750, # tenant-writable, but we skip drop_privileges) and every call # after (websites is root:root 0o755, only root can mkdir). # The leaf mode 0o755 matches the sealed shape it will settle # into so a legitimate root:root leaf exists whether or not the # fchown below succeeds (dev/unit-test env). leaf_fd = _mkdir_nofollow_under( websites_fd, get_website_id(document_root), 0o755 ) # F-15 (CLOS-5953) DiD: seal ``.cagefs/websites`` and the per- # website leaf as root:root mode 0755. The pre-mkdir walk above # already rejects a symlink planted BEFORE this call, and # sealing the leaf alone was not enough: rename/unlink/create- # child permissions live on the PARENT inode, so a tenant-owned # ``.cagefs/websites`` parent would still let the tenant `mv` # the leaf aside and put a symlink in its place, at which point # MS_BIND on a child of the replacement leaf would follow the # symlink and mount an arbitrary host path into the isolated # namespace. Sealing ``websites`` blocks the rename/replace at # that boundary. # # Note (Bugbot HIGH follow-up): do NOT seal ``.cagefs`` itself — # CageFS proper owns that as user:user mode 0771 for its own # model (``update_status``, ``configure_alt_php``, PHP selector # paths, ``cagefs_enter``). Sealing ``.cagefs`` to root breaks # those tenant-side flows. ``websites`` is a # website-isolation-only subdir, so re-owning it is safe. # Uses fchown/fchmod on the fds captured during the walk (bound # to the real inodes) — no path re-resolve. Non-root callers # (unit tests, dev harness) get EPERM and the seal is a no-op; # the production entry point runs as root via cagefsctl so this # branch always applies there. Sealed order is inside-out (leaf # → websites) so a partial failure still leaves the leaf # sealed. Mode 0755 lets the tenant traverse to descendants # that root subsequently creates (bind mount points still work) # but blocks unlink/rename/create at each sealed level. for fd_to_seal in (leaf_fd, websites_fd): if fd_to_seal is None: continue try: os.fchown(fd_to_seal, 0, 0) os.fchmod(fd_to_seal, 0o755) except PermissionError: pass finally: for fd in (leaf_fd, websites_fd, cagefs_fd): if fd is not None: os.close(fd) os.close(home_fd) def seal_overlay_storage_ancestors(user: str, document_root: str) -> None: """ Re-apply the F-15 (CLOS-5953) DiD seal on the overlay-storage ancestor chain and the per-website leaf. Idempotent by design: it re-runs the same fd-bound fchown/fchmod the initial ``create_overlay_storage_directory`` did, plus the same O_NOFOLLOW walk. Callers use it AFTER any provisioning step that can re-chown the leaf back to the tenant. In particular ``cagefsctl --rebuild-alt-php-ini`` (called during ``enable_website_isolation``) reaches ``selector.configure.configure_alt_php`` which invokes ``make_userdir(<.cagefs>/websites/<website_id>, 0o771, uid, gid)`` -- that resets the leaf to tenant:tenant 0o771 immediately before ``write_jail_mounts_config`` runs, undoing the initial seal. Re- calling this after the alt_php step restores root:root 0o755 on the leaf so a tenant symlink swap between then and the root-run bind mounter cannot land. Silent no-op when the overlay tree does not exist (isolation not enabled for this website, or teardown path). Non-root callers (unit tests, dev harness) get EPERM and the seal becomes a no-op; the production entry point runs as root via cagefsctl. """ pw = ClPwd().get_pw_by_name(user) if not pw or not Path(pw.pw_dir).exists(): return website_id = get_website_id(document_root) try: home_fd = os.open( pw.pw_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC ) except OSError: return cagefs_fd = websites_fd = leaf_fd = None try: try: cagefs_fd = os.open( ".cagefs", os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY | os.O_CLOEXEC, dir_fd=home_fd, ) except OSError: return try: websites_fd = os.open( "websites", os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY | os.O_CLOEXEC, dir_fd=cagefs_fd, ) except OSError: return try: leaf_fd = os.open( website_id, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY | os.O_CLOEXEC, dir_fd=websites_fd, ) except OSError: leaf_fd = None for fd_to_seal in (leaf_fd, websites_fd): if fd_to_seal is None: continue try: os.fchown(fd_to_seal, 0, 0) os.fchmod(fd_to_seal, 0o755) except PermissionError: pass finally: for fd in (leaf_fd, websites_fd, cagefs_fd): if fd is not None: os.close(fd) os.close(home_fd) def remove_website_token_directory(user: str, document_root: str): """ Remove website token directory structure and files. """ website_base_dir = Path(get_user_var_cagefs_path(user)) / ".cagefs/website" website_dir = website_base_dir / get_website_id(document_root) if website_dir.exists(): shutil.rmtree(website_dir) def website_suffix_with_hash(docroot): """ Returns path: websites/<document_root_hash> """ return os.path.join("websites", get_website_id(docroot)) def full_website_path(homedir, docroot): """ Returns <homedir>/.cagefs/websites/<document_root_hash> """ return os.path.join(homedir, ".cagefs", website_suffix_with_hash(docroot)) def invalidate_ns_cache(user: str, document_root: str): """ Removes cached namespace from disk """ website_base_dir = Path(get_user_var_cagefs_path(user)) / ".cagefs/website" website_dir = website_base_dir / get_website_id(document_root) (website_dir / ".cagefs.mnt").unlink(missing_ok=True) def _generate_password(length): """ Generate a random password/token using the same algorithm as the C function. Uses cryptographically secure random bytes and converts them to alphanumeric characters. """ if length == 0 or length > 256: raise ValueError("Invalid buffer length requested") charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" charset_size = len(charset) # Generate random bytes random_bytes = secrets.token_bytes(length) # Convert bytes to alphanumeric characters result = "".join(charset[b % charset_size] for b in random_bytes) return result