haxorqt
Home
Console
Upload
information
Create File
Create Folder
About
:
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
clcagefslib
/
webisolation
/
crontab
/
Filename :
processor.py
back
Copy
# -*- coding: utf-8 -*- # # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT # """Processing functions for crontab operations.""" import os import pwd import shlex import subprocess import sys from typing import BinaryIO, Callable, Optional from clcagefslib.domain import is_isolation_enabled from .constants import ISOLATION_WRAPPER from .parser import ( parse_crontab_structure, write_crontab_structure, entries_to_str_list, ) from .structure import EnvAssignmentLine, ParsedCrontabLine from .utils import get_document_root CRONTAB_BIN = "/usr/bin/crontab" def _authenticate_caller_username() -> str: """Return the crontab owner's system name authoritatively. F-10 (CLOS-5948) DiD: ``pwd.getpwuid(os.getuid()).pw_name`` returns whichever passwd entry NSS iteration surfaces first for the caller's UID. In same-UID CageFS sibling configurations (multiple accounts sharing a numeric UID), that lookup can resolve to a *different* sibling than the actual caller, letting the CRONTAB_* proxy path read or replace another sibling's crontab. The Python wrapper cannot recover the trusted socket-authenticated name from proxyexec's cage.server dispatcher after ``setuid`` — the only user-space signal that survives the crossing is ``PROXYEXEC_UID``, which is numeric and thus ambiguous in same-UID configs. Since Vixie's ``crontab`` binary also derives the caller name from ``getpwuid(getuid())`` (so ``crontab -u <name>`` cannot disambiguate either), the safe stance is to REFUSE the operation when the UID resolves to more than one system name. Callers get a class-neutral ``RuntimeError`` to surface as a plain error, never a Python traceback. """ uid = os.getuid() matching_names = [ entry.pw_name for entry in pwd.getpwall() if entry.pw_uid == uid ] if len(matching_names) != 1: # Zero matches (no passwd entry for our own UID — unreachable # in a healthy CageFS jail) OR more than one (same-UID sibling # configuration — cannot determine the real caller from UID # alone). raise RuntimeError("Operation not permitted.") return matching_names[0] def process_list(stdout: Optional[BinaryIO] = None, stderr: Optional[BinaryIO] = None) -> int: """ Process CRONTAB_LIST command. Runs 'crontab -l' to get current crontab entries. When isolation is active (PROXYEXEC_DOCUMENT_ROOT is set), only shows entries for the current document root. Removes isolation prefixes from output. Args: stdout: Output stream buffer (defaults to sys.stdout.buffer) stderr: Error stream buffer (defaults to sys.stderr.buffer) Returns: int: Exit code from crontab command, or 1 on error """ stdout = stdout or sys.stdout.buffer stderr = stderr or sys.stderr.buffer username = _authenticate_caller_username() # F-10 (CLOS-5948) DiD: Vixie/cronie rejects ``-u`` unless the real # UID is root (``must be privileged to use -u``), and the # CRONTAB_LIST proxy path runs after proxyexec ``setuid``s to the # caller — passing ``-u`` here would break every legitimate list # operation. The uniqueness check in ``_authenticate_caller_username`` # is the real defence: it refuses to invoke ``crontab`` at all when # the caller's UID resolves to more than one passwd name, which is # the same-UID sibling collision the finding described. result = subprocess.run( [CRONTAB_BIN, "-l"], capture_output=True, ) # early exit in case site isolation is not turned on if not is_isolation_enabled(username): stdout.write(result.stdout) stderr.write(result.stderr) return result.returncode if result.returncode != 0: # Pass through stderr from crontab stderr.write(result.stderr) return result.returncode document_root = get_document_root() # Parse structure and pick entries to show structure = parse_crontab_structure(result.stdout) if document_root is not None: entries_to_show = structure.docroot_sections.get(document_root, []) else: entries_to_show = structure.global_records # Convert selected entries to bytes, removing wrapper prefixes if isolation is active result_parts = entries_to_str_list(entries_to_show, without_wrapper=bool(document_root)) output_data = b"".join(result_parts) stdout.write(output_data) return 0 # scanner-triage: docroot authz and local uid drop both close upstream. # get_document_root (in .utils) authenticates the caller against # userdomains(username), and PROXYEXEC_DOCUMENT_ROOT itself is set # server-side from a validated .cagefs.website token — not from the # tenant's env. The local uid drop is done by the proxyexec dispatcher: # CRONTAB_* aliases carry `:secure:noproceed` (never `root:`), so # setuid(pw_uid) always fires before execv and this code runs as the # caller. Refile if any CRONTAB_* alias ever acquires a `root:` prefix, # or if an admin-side caller imports process_save with EUID 0. def process_save( stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None, stderr: Optional[BinaryIO] = None, run_func: Optional[Callable] = None, ) -> int: """ Process CRONTAB_SAVE command. Reads crontab entries from stdin. If isolation is active: 1. Gets the current full crontab 2. Removes entries for the current document root 3. Adds new entries to the current document root section 4. Merges and saves the result in new format If isolation is not active: 1. Gets the current full crontab 2. Replaces user records section with new entries 3. Preserves all docroot sections 4. Saves in new format This ensures entries for other document roots are preserved. Args: stdin: Input stream buffer (defaults to sys.stdin.buffer) stdout: Output stream buffer (defaults to sys.stdout.buffer) stderr: Error stream buffer (defaults to sys.stderr.buffer) run_func: Function to run subprocess (defaults to subprocess.run) Returns: int: Exit code from crontab command, or 1 on error """ stdin = stdin or sys.stdin.buffer stdout = stdout or sys.stdout.buffer stderr = stderr or sys.stderr.buffer run_func = run_func or subprocess.run # scanner-triage: this runs after the proxyexec dispatcher has entered # the caller's LVE and setuid'd to their UID (CRONTAB_SAVE has no # `nolve` flag), so every byte read is charged to the caller's own # LVE — kernel SIGKILLs on overrun. Blowing up your own LVE is # user-to-self, not a cross-tenant DoS. input_data = stdin.read() document_root = get_document_root() if document_root is not None and ('\n' in document_root or '\r' in document_root): raise ValueError(f'Invalid document root: {document_root!r}') username = _authenticate_caller_username() if is_isolation_enabled(username): # Get current crontab to preserve entries # F-10 (CLOS-5948) DiD: no ``-u`` — see the note in # ``process_list`` above. The uniqueness check in # ``_authenticate_caller_username`` is the real defence. list_result = run_func( [CRONTAB_BIN, "-l"], capture_output=True, ) # No existing crontab or error - start fresh # Note: returncode 1 typically means "no crontab for user", which is acceptable existing_data = list_result.stdout if list_result.returncode == 0 else b"" # Parse existing crontab into structure existing_structure = parse_crontab_structure(existing_data) # Parse new input (filtered data, no section markers) # input_data is already bytes from stdin.read() new_structure = parse_crontab_structure(input_data) # Extract entries and add wrapper prefixes for docroot sections parsed_entries = [] for entry in new_structure.global_records: # Drop env-assignment lines (`SHELL=`, `PATH=`, `HOME=`, `MAILTO=`, # ...) from per-site sections — crond honours them in textual # order and would apply them to following job lines, running an # attacker-controlled SHELL before the isolation wrapper and # bypassing per-docroot CageFS scope. Env assignments are only # safe in the user's global section, never inside an isolated # docroot block. F-09 (CLOS-5947): the classifier now also # recognises the `"NAME"=` and `'NAME'=` quoted forms that # vixie-cron's load_env accepts, so those forms are dropped # here too. if document_root and isinstance(entry, EnvAssignmentLine): continue if isinstance(entry, ParsedCrontabLine) and document_root: # Decode command to string for shlex.quote() command_bytes = entry.command has_newline = command_bytes.endswith(b"\n") command_str = command_bytes.rstrip(b"\n").decode("utf-8", errors="replace") # Use shlex.quote() to properly quote the command for bash -c quoted_command = shlex.quote(command_str) # Build the wrapped command: wrapper path docroot bash -c "quoted_command" # Use shlex.join() for the prefix to handle paths with spaces, then add quoted command prefix_parts = shlex.join([ISOLATION_WRAPPER, document_root, "bash", "-c"]) wrapped_command_str = f"{prefix_parts} {quoted_command}" wrapped_command = wrapped_command_str.encode("utf-8") if has_newline: wrapped_command += b"\n" parsed_entries.append( ParsedCrontabLine(schedule=entry.schedule, command=wrapped_command) ) else: # Keep as-is for comments and when document_root is None (may have wrapper prefixes) parsed_entries.append(entry) if document_root: # Isolation active: replace docroot section with new entries existing_structure.docroot_sections[document_root] = parsed_entries else: existing_structure.global_records = parsed_entries # Write in new format modified_data = write_crontab_structure(existing_structure) else: # pass through modified_data = input_data # F-10 (CLOS-5948) DiD: no ``-u`` here either — Vixie/cronie rejects # ``-u`` under a setuid'd caller. The uniqueness check earlier in # this function (via ``_authenticate_caller_username``) is what # closes the same-UID sibling collision. result = run_func( [CRONTAB_BIN, "-"], input=modified_data, capture_output=True, ) # Pass through any output from crontab if result.stdout: stdout.write(result.stdout) if result.stderr: stderr.write(result.stderr) return result.returncode