import os
import re
from . import local_api, staging
# Pattern for safe IDs (task_id, system_id) — alphanumeric, underscores, hyphens only.
# Used to prevent path traversal and shell injection.
safe_id_pattern = re.compile(r'^[a-zA-Z0-9_\-]+$')
# Allowed path prefixes for PHP ini_scan_dir / ini_location.
# Used to prevent root writing xray.ini to arbitrary paths
# via attacker-controlled PHP_INI_SCAN_DIR.
ALLOWED_INI_PREFIXES = (
'/opt/alt/php',
'/var/cagefs/',
'/opt/cpanel/ea-php',
'/opt/plesk/php/',
'/usr/local/php',
'/usr/share/cagefs/',
'/usr/share/cagefs-skeleton/',
'/etc/cl.php.d/',
'/etc/php.d/',
'/etc/php.scan.d/',
# OPERATOR-defined per-domain scan dirs, of the shape
# //php/ — see phpinfo_utils._PER_DOMAIN_INI_ROOTS for
# where they come from and why '/etc/users/' alone was not enough. Narrowed to
# the requesting account's own subtree by
# phpinfo_utils._is_own_per_domain_ini_dir; this prefix list is only the outer
# bound.
'/etc/users/',
'/etc/configs/users/',
)
def canonical_variants(path: str) -> tuple[str, ...]:
"""The literal ``path`` plus its realpath, when they differ.
An allowlisted root may itself be reached through a symlink: the documented
way to move CageFS to another partition is ``ln -s /home/cagefs /var/cagefs``
(likewise for /usr/share/cagefs-skeleton). Callers compare a realpath'd
CANDIDATE against these roots, so the roots must be offered in canonical form
too, or every path under a relocated root fails the comparison (CLOS-7045).
A trailing separator is preserved so directory-boundary semantics carry over.
This does not widen any gate when the relocation target is a dedicated,
root-controlled directory: the set of inodes accepted is exactly the one
the literal root already named. Resolve on every call so an administrator's
runtime relocation takes effect without restarting the daemon.
"""
real = os.path.realpath(path)
if path.endswith('/') and not real.endswith('/'):
real += '/'
return (path,) if real == path else (path, real)
def _matches_prefix(resolved: str, prefix: str) -> bool:
normalized = os.path.normpath(prefix)
if prefix.endswith('/'):
# Full-directory prefix: candidate must be the directory itself
# or a descendant of it.
return resolved == normalized or resolved.startswith(normalized + os.sep)
# Bare component prefix: the remainder must be a run of version
# digits terminated by end-of-path or a separator, so the versioned
# directory '/opt/alt/php80' (and its subdirs) is accepted while
# siblings whose name extends the component — '/opt/alt/phpevil'
# (letter), '/usr/local/php-evil' ('-') or '/opt/alt/php80evil'
# (trailing non-version chars) — are rejected.
if not resolved.startswith(prefix):
return False
rest = resolved[len(prefix) :]
digits = rest[: len(rest) - len(rest.lstrip('0123456789'))]
return bool(digits) and rest[len(digits) : len(digits) + 1] in ('', os.sep)
def is_allowed_ini_path(path: str) -> bool:
"""Boundary-aware allowlist check for a PHP ini_scan_dir / ini_location.
A plain ``startswith`` against ALLOWED_INI_PREFIXES is unsafe: a sibling
such as ``/opt/alt/phpevil`` string-prefix-matches the bare ``/opt/alt/php``
entry and escapes the intended tree. Canonicalize the candidate and require
a real directory boundary against each prefix:
* Prefixes that name a full directory (trailing separator) match only when
the candidate is that directory or descends below it.
* The bare component prefixes (``/opt/alt/php``, ``/opt/cpanel/ea-php``,
``/usr/local/php``) are followed by a PHP version component; the candidate
must continue with a run of version digits (e.g. ``/opt/alt/php80``) that
then either ends or descends at a separator — never a letter or other
separator-extending name.
Each prefix is compared in both its literal and canonical spelling
(canonical_variants), so a root relocated by symlink per the docs still
matches the realpath'd candidate.
"""
resolved = os.path.realpath(path)
return any(
_matches_prefix(resolved, variant) for prefix in ALLOWED_INI_PREFIXES for variant in canonical_variants(prefix)
)
local_tasks_storage = '/usr/share/alt-php-xray/tasks'
fpm_stat_storage = '/usr/share/alt-php-xray/fpm.stat'
nginx_cache_stat_storage = '/usr/share/alt-php-xray/nginx-user-cache.stat'
continuous_storage = '/usr/share/alt-php-xray/continuous'
request_data_storage = '/usr/share/alt-php-xray/requests'
tasks_base_storage = '/usr/share/alt-php-xray-tasks'
advice_pending_storage = '/usr/share/alt-php-xray/pending'
advice_processed_storage = '/usr/share/alt-php-xray/applied_data'
advice_list_cache = '/usr/share/alt-php-xray/advices_cache.json'
advice_list_im360_cache = '/usr/share/alt-php-xray/advices_im360_cache.json'
manager_log = '/var/log/alt-php-xray/manager.log'
agent_log = '/var/log/alt-php-xray/agent.log'
user_agent_log = '/var/log/alt-php-xray/user-agent.log'
adviser_log = '/var/log/alt-php-xray/smart_advice.log'
agent_sock = '/opt/alt/php-xray/run/xray.sock'
user_agent_sock = '/opt/alt/php-xray/run/xray-user.sock'
agent_file = '/usr/share/alt-php-xray/agent.file'
mail_template_location = '/usr/share/alt-php-xray/mail_templates'
mail_scripts_location = '/usr/share/alt-php-xray/get_email_scripts'
jwt_token_location = '/etc/sysconfig/rhn/jwt.token'
drop_after = 80 # minutes
check_period = 20 # minutes
throttling_threshold = 200 # milliseconds
fpm_reload_timeout = 1 # minutes
user_tasks_count = 1
allow_disable_nginx_cache = True
logging_level = 'debug' if os.environ.get('DEBUG_LOG') else 'info'
proto = 'https'
if staging():
s_postfix = '8'
s_key = '2ac05adf1e9f4cd18c064fe9e9b1a474'
s_name = 'cl.sentry.cloudlinux.com'
if local_api():
proto = 'http'
api_server = '127.0.0.1:8000'
else:
api_server = 'test-api.imunify360.com'
adviser_api_server = 'x-ray-staging.cloudlinux.com'
else:
s_postfix = '7'
s_key = 'c4d18de9e2164ca6b92cf110faec42ea'
s_name = 'cl.sentry.cloudlinux.com'
api_server = 'xray.cloudlinux.com'
adviser_api_server = 'x-ray-advice.cloudlinux.com'
sentry_dsn = f'https://{s_key}@{s_name}/{s_postfix}'
advice_action_sources = ['ACCELERATE_WP', 'WORDPRESS_PLUGIN']
advice_reason_max_len = 200
part_delimiter = ':'
task_delimiter = ','