9Vj Z d Z ddlmZ ddlZddlZ ej d ZdZdd Zd
dZ dS )ab 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
(`[]` 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.
)annotationsNz^/[A-Za-z0-9_./-]*$i docrootstrreturnc t | t st d| | st d t | t k r*t dt | dt d| | d st d| t | st d| | d D ]}|d k rt d
| | S )a1 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, `_`, `-`,
`.`, `/`).
z Invalid docroot (not a string): zInvalid docroot: empty stringzInvalid docroot (length z exceeds ): /z Invalid docroot (not absolute): z)Invalid docroot (disallowed characters): z..z$Invalid docroot (parent traversal): )
isinstancer
ValueErrorlen_DOCROOT_MAX_LEN
startswith_DOCROOT_ALLOWED_REmatchsplit)r segments _opt/cloudlinux/venv/lib/python3.11/site-packages/clcagefslib/webisolation/docroot_validation.pyvalidate_docrootr 3 s3 " gs## IGGGGHHH :8999
7||&&&^s7||^^>N^^SZ^^
c"" IGGGGHHH$$W-- RPWPPQQQ ==%% Q Qd??OGOOPPP N allowed_prefixc J t j | d }t j |d }n(# t $ r}t d| d| |d}~ww xY w||k r0| |dz st d|d|d| | S ) a 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//public_html/`` on cPanel / DA / Plesk)
pre-aim the bind source at an arbitrary host path
(``ln -s / /home//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).
F)strictz!Invalid docroot (cannot resolve: r Nr zInvalid docroot (resolves to z outside allowed prefix )ospathrealpathOSErrorr r )r r resolved_docrootresolved_prefixexcs r validate_docroot_no_symlinksr [ s r7++GE+BB'**>%*HH CCCCC
?**3C3N3N#4 4*
@,<