| Server IP : 142.11.234.102 / Your IP : 216.73.217.78 Web Server : Apache System : Linux dal-shared-66.hostwindsdns.com 4.18.0-513.24.1.lve.1.el8.x86_64 #1 SMP Thu May 9 15:10:09 UTC 2024 x86_64 User : krnuyqrm ( 1183) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : OFF | Pkexec : OFF Directory : /opt/cloudlinux/venv/lib/python3.11/site-packages/clcagefslib/webisolation/ |
Upload File : |
#!/opt/cloudlinux/venv/bin/python3 -sbb
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2026 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
"""Trust-boundary validation for panel-supplied document root strings.
The docroot value originates from the hosting panel (cPanel /
DirectAdmin / Plesk) and is consumed by privileged code that writes
jail.c mount configuration files read by root. The jail.c mount syntax
is whitespace-delimited (MountEntry.render in mount_types.py joins
source/target/options with spaces) and section headers are bracketed
(`[<docroot>]` in jail_config.MountConfig.render), so any whitespace,
control character, newline, or bracket inside the docroot corrupts the
parser. A sibling module already rejects newlines/carriage-returns on
the analogous crontab write path (crontab/libhooks.py and
crontab/parser.py); this module is the equivalent guard for the jail
mount config write path.
Validation is centralized at the trust boundary - call sites are
`enable_website_isolation` (where a tenant-owned domain is resolved to
a docroot for the first time) and `write_jail_mounts_config` (where
the docroot map is re-read from the panel for every regeneration). The
helper raises ValueError on rejection, matching the sibling crontab
pattern.
"""
from __future__ import annotations
import os
import re
# Strict allowlist: alnum, `_`, `-`, `.`, `/`. This excludes whitespace
# (which would split mount-line tokens), brackets (which would close the
# jail section header), quotes, and any control character. Production
# docroots in shared-hosting deployments are conventionally of the form
# `/home/<user>/<subdir>` and well inside this allowlist; anything
# outside it is either a panel misconfiguration or an injection attempt.
_DOCROOT_ALLOWED_RE = re.compile(r"^/[A-Za-z0-9_./-]*$")
# Hard cap to bound the size of strings that flow into mount lines and
# section headers. A 4 KiB ceiling is well above any realistic docroot
# (Linux PATH_MAX is 4096) and well below pathological mount-line
# sizes.
_DOCROOT_MAX_LEN = 4096
def validate_docroot(docroot: str) -> str:
"""Validate a panel-supplied document root before it reaches the
jail mount config writer.
Args:
docroot: Document root string returned by the panel (e.g. from
``clcommon.cpapi.docroot`` or ``clcommon.cpapi.userdomains``).
Returns:
The validated docroot string, unchanged.
Raises:
ValueError: If the docroot is empty, not an absolute path, too
long, contains a path-traversal segment, or contains any
character outside the strict allowlist (alnum, `_`, `-`,
`.`, `/`).
"""
if not isinstance(docroot, str):
raise ValueError(f"Invalid docroot (not a string): {docroot!r}")
if not docroot:
raise ValueError("Invalid docroot: empty string")
if len(docroot) > _DOCROOT_MAX_LEN:
raise ValueError(
f"Invalid docroot (length {len(docroot)} exceeds {_DOCROOT_MAX_LEN}): {docroot!r}"
)
if not docroot.startswith("/"):
raise ValueError(f"Invalid docroot (not absolute): {docroot!r}")
if not _DOCROOT_ALLOWED_RE.match(docroot):
raise ValueError(f"Invalid docroot (disallowed characters): {docroot!r}")
# Reject traversal segments. The allowlist permits `.` and `..` as
# path components even though it forbids most metacharacters, so
# screen explicitly: `..` lets a tenant's owned-domain docroot point
# at a sibling tenant's tree once it is bind-mounted as the jail
# source.
for segment in docroot.split("/"):
if segment == "..":
raise ValueError(f"Invalid docroot (parent traversal): {docroot!r}")
return docroot
def validate_docroot_no_symlinks(docroot: str, allowed_prefix: str) -> str:
"""Reject a docroot whose on-disk path escapes ``allowed_prefix``.
The lexical ``validate_docroot`` above pins the *string shape* of a
panel-supplied docroot, but the value still flows verbatim into
jail.c mount-line ``source`` fields and is later consumed by a
root-run mount executor that calls ``bind(2)``. Per Linux mount(2)
semantics, MS_BIND on a symlinked source dereferences the symlink
and bind-mounts the resolved path - which lets a tenant who can
write anywhere along the docroot's ancestor chain
(typically ``/home/<user>/public_html/<sub>`` on cPanel / DA / Plesk)
pre-aim the bind source at an arbitrary host path
(``ln -s / /home/<user>/public_html/evil``). The root-run jail then
happily bind-mounts ``/`` (or any other operator path the tenant
chose) inside the tenant's isolated namespace, exposing
``/etc/shadow``, other tenants' homes, etc.
The fix shape is the playbook's ``realpath() + assert the resolved
path begins with the allowed prefix``: canonicalise both the
docroot and the allowed prefix (so a benign operator-installed
symlink like ``/home -> /home2`` does not produce a false reject),
then require that the resolved docroot stays under the resolved
prefix. A tenant-planted symlink aimed outside the home tree
(``/home/u/public_html/evil -> /``) resolves to ``/``, which is
not a descendant of the user's resolved home and is rejected.
The sibling ``create_overlay_storage_directory`` already takes
the equivalent O_NOFOLLOW component-walk stance (see
``py/clcagefslib/webisolation/jail_utils.py``); this is the
docroot-side analogue called at the same trust boundary as
``validate_docroot``.
Args:
docroot: Document root string returned by the panel. Must
already have passed the lexical ``validate_docroot``
check (so it is absolute, ``..``-free, and within the
character allowlist) - this function does *not* re-run
those checks.
allowed_prefix: Absolute path the resolved docroot must be
inside. Typically the user's home directory; passing the
unresolved value is fine because the function canonicalises
it too.
Returns:
The docroot string, unchanged.
Raises:
ValueError: If the resolved docroot escapes the resolved
``allowed_prefix``, or if either path cannot be
canonicalised (e.g. because of an OS error other than
ENOENT).
"""
# strict=False so a not-yet-existent leaf does not raise; bind(2)
# resolves whatever exists on disk at mount time, and an entirely
# absent path resolves to itself - safe. The dangerous case is
# "some ancestor IS a symlink that points outside the user tree",
# which realpath surfaces by returning a path outside the resolved
# prefix.
try:
resolved_docroot = os.path.realpath(docroot, strict=False)
resolved_prefix = os.path.realpath(allowed_prefix, strict=False)
except OSError as exc:
raise ValueError(
f"Invalid docroot (cannot resolve: {exc}): {docroot!r}"
) from exc
# Component-aware prefix check: ``resolved_docroot == prefix`` (the
# user's home itself is a valid docroot in some panel
# configurations) OR ``resolved_docroot`` starts with
# ``prefix + '/'`` so ``/home/userfoo`` does not pass for prefix
# ``/home/user``.
if resolved_docroot != resolved_prefix and not resolved_docroot.startswith(
resolved_prefix + "/"
):
raise ValueError(
f"Invalid docroot (resolves to {resolved_docroot!r} outside "
f"allowed prefix {resolved_prefix!r}): {docroot!r}"
)
return docroot