/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
/opt/cloudlinux/venv/lib/python3.11/site-packages
mkdir
upload
Name
Size
Mode
Actions
aiohttp/
-
0755
rm
aiohttp-3.9.2.dist-info/
-
0755
rm
aiohttp_jinja2/
-
0755
rm
aiohttp_jinja2-1.5.dist-info/
-
0755
rm
aiohttp_security/
-
0755
rm
aiohttp_security-0.4.0.dist-info/
-
0755
rm
aiohttp_session/
-
0755
rm
aiohttp_session-2.9.0.dist-info/
-
0755
rm
aiosignal/
-
0755
rm
aiosignal-1.3.1.dist-info/
-
0755
rm
alembic/
-
0755
rm
alembic-1.11.1.dist-info/
-
0755
rm
astroid/
-
0755
rm
astroid-2.15.6.dist-info/
-
0755
rm
attr/
-
0755
rm
attrs/
-
0755
rm
attrs-23.1.0.dist-info/
-
0755
rm
backports/
-
0755
rm
certifi/
-
0755
rm
certifi-2023.7.22.dist-info/
-
0755
rm
cffi/
-
0755
rm
cffi-1.15.1.dist-info/
-
0755
rm
chardet/
-
0755
rm
chardet-5.2.0.dist-info/
-
0755
rm
charset_normalizer/
-
0755
rm
charset_normalizer-2.1.1.dist-info/
-
0755
rm
clcagefslib/
-
0755
rm
clcommon/
-
0755
rm
clconfig/
-
0755
rm
clconfigure/
-
0755
rm
cldashboard/
-
0755
rm
clevents/
-
0755
rm
clflags/
-
0755
rm
cllicense/
-
0755
rm
cllimits/
-
0755
rm
cllimitslib_v2/
-
0755
rm
cllimits_validator/
-
0755
rm
cllvectl/
-
0755
rm
clpackages/
-
0755
rm
clquota/
-
0755
rm
clselect/
-
0755
rm
clselector/
-
0755
rm
clsentry/
-
0755
rm
clsummary/
-
0755
rm
clveconfig/
-
0755
rm
clwizard/
-
0755
rm
clwpos/
-
0755
rm
cl_dom_collector/
-
0755
rm
cl_website_collector/
-
0755
rm
configparser-5.0.2.dist-info/
-
0755
rm
contextlib2/
-
0755
rm
contextlib2-21.6.0.dist-info/
-
0755
rm
coverage/
-
0755
rm
coverage-7.2.7.dist-info/
-
0755
rm
cryptography/
-
0755
rm
cryptography-41.0.2.dist-info/
-
0755
rm
ddt-1.4.4.dist-info/
-
0755
rm
dill/
-
0755
rm
dill-0.3.7.dist-info/
-
0755
rm
distlib/
-
0755
rm
distlib-0.3.8.dist-info/
-
0755
rm
docopt-0.6.2.dist-info/
-
0755
rm
dodgy/
-
0755
rm
dodgy-0.2.1.dist-info/
-
0755
rm
filelock/
-
0755
rm
filelock-3.13.1.dist-info/
-
0755
rm
flake8/
-
0755
rm
flake8-5.0.4.dist-info/
-
0755
rm
flake8_polyfill/
-
0755
rm
flake8_polyfill-1.0.2.dist-info/
-
0755
rm
frozenlist/
-
0755
rm
frozenlist-1.4.0.dist-info/
-
0755
rm
future/
-
0755
rm
future-0.18.3.dist-info/
-
0755
rm
git/
-
0755
rm
gitdb/
-
0755
rm
gitdb-4.0.10.dist-info/
-
0755
rm
GitPython-3.1.32.dist-info/
-
0755
rm
guppy/
-
0755
rm
guppy3-3.1.3.dist-info/
-
0755
rm
idna/
-
0755
rm
idna-3.4.dist-info/
-
0755
rm
iniconfig/
-
0755
rm
iniconfig-2.0.0.dist-info/
-
0755
rm
isort/
-
0755
rm
isort-5.12.0.dist-info/
-
0755
rm
jinja2/
-
0755
rm
Jinja2-3.0.3.dist-info/
-
0755
rm
jsonschema/
-
0755
rm
jsonschema-3.2.0.dist-info/
-
0755
rm
jwt/
-
0755
rm
lazy_object_proxy/
-
0755
rm
lazy_object_proxy-1.9.0.dist-info/
-
0755
rm
libfuturize/
-
0755
rm
libpasteurize/
-
0755
rm
lvemanager/
-
0755
rm
lvestats/
-
0755
rm
lve_stats-2.0.dist-info/
-
0755
rm
lve_utils/
-
0755
rm
lxml/
-
0755
rm
lxml-4.9.2.dist-info/
-
0755
rm
mako/
-
0755
rm
Mako-1.2.4.dist-info/
-
0755
rm
markupsafe/
-
0755
rm
MarkupSafe-2.1.3.dist-info/
-
0755
rm
mccabe-0.7.0.dist-info/
-
0755
rm
mock/
-
0755
rm
mock-5.1.0.dist-info/
-
0755
rm
multidict/
-
0755
rm
multidict-6.0.4.dist-info/
-
0755
rm
numpy/
-
0755
rm
numpy-1.25.1.dist-info/
-
0755
rm
numpy.libs/
-
0755
rm
packaging/
-
0755
rm
packaging-23.1.dist-info/
-
0755
rm
past/
-
0755
rm
pep8_naming-0.10.0.dist-info/
-
0755
rm
pip/
-
0755
rm
pip-24.3.1.dist-info/
-
0755
rm
pkg_resources/
-
0755
rm
platformdirs/
-
0755
rm
platformdirs-3.11.0.dist-info/
-
0755
rm
pluggy/
-
0755
rm
pluggy-1.2.0.dist-info/
-
0755
rm
prettytable/
-
0755
rm
prettytable-3.8.0.dist-info/
-
0755
rm
prometheus_client/
-
0755
rm
prometheus_client-0.8.0.dist-info/
-
0755
rm
prospector/
-
0755
rm
prospector-1.10.2.dist-info/
-
0755
rm
psutil/
-
0755
rm
psutil-5.9.5.dist-info/
-
0755
rm
psycopg2/
-
0755
rm
psycopg2_binary-2.9.6.dist-info/
-
0755
rm
psycopg2_binary.libs/
-
0755
rm
pycodestyle-2.9.1.dist-info/
-
0755
rm
pycparser/
-
0755
rm
pycparser-2.21.dist-info/
-
0755
rm
pydocstyle/
-
0755
rm
pydocstyle-6.3.0.dist-info/
-
0755
rm
pyfakefs/
-
0755
rm
pyfakefs-5.10.2.dist-info/
-
0755
rm
pyflakes/
-
0755
rm
pyflakes-2.5.0.dist-info/
-
0755
rm
PyJWT-2.8.0.dist-info/
-
0755
rm
pylint/
-
0755
rm
pylint-2.17.4.dist-info/
-
0755
rm
pylint_celery/
-
0755
rm
pylint_celery-0.3.dist-info/
-
0755
rm
pylint_django/
-
0755
rm
pylint_django-2.5.3.dist-info/
-
0755
rm
pylint_flask/
-
0755
rm
pylint_flask-0.6.dist-info/
-
0755
rm
pylint_plugin_utils/
-
0755
rm
pylint_plugin_utils-0.7.dist-info/
-
0755
rm
pylve-2.1-py3.11.egg-info/
-
0755
rm
pymysql/
-
0755
rm
PyMySQL-1.1.0.dist-info/
-
0755
rm
pyparsing/
-
0755
rm
pyparsing-3.0.9.dist-info/
-
0755
rm
pyrsistent/
-
0755
rm
pyrsistent-0.19.3.dist-info/
-
0755
rm
pytest/
-
0755
rm
pytest-7.4.0.dist-info/
-
0755
rm
pytest_check/
-
0755
rm
pytest_check-2.5.3.dist-info/
-
0755
rm
pytest_snapshot/
-
0755
rm
pytest_snapshot-0.9.0.dist-info/
-
0755
rm
pytest_subprocess/
-
0755
rm
pytest_subprocess-1.5.3.dist-info/
-
0755
rm
pytest_tap/
-
0755
rm
pytest_tap-3.5.dist-info/
-
0755
rm
python_pam-1.8.4.dist-info/
-
0755
rm
pyvirtualdisplay/
-
0755
rm
PyVirtualDisplay-3.0.dist-info/
-
0755
rm
PyYAML-6.0.1.dist-info/
-
0755
rm
raven/
-
0755
rm
raven-6.10.0.dist-info/
-
0755
rm
requests/
-
0755
rm
requests-2.31.0.dist-info/
-
0755
rm
requirements_detector/
-
0755
rm
requirements_detector-1.2.2.dist-info/
-
0755
rm
schema-0.7.5.dist-info/
-
0755
rm
semver/
-
0755
rm
semver-3.0.1.dist-info/
-
0755
rm
sentry_sdk/
-
0755
rm
sentry_sdk-1.29.2.dist-info/
-
0755
rm
setoptconf/
-
0755
rm
setoptconf_tmp-0.3.1.dist-info/
-
0755
rm
setuptools/
-
0755
rm
setuptools-81.0.0.dist-info/
-
0755
rm
simplejson/
-
0755
rm
simplejson-3.19.1.dist-info/
-
0755
rm
six-1.16.0.dist-info/
-
0755
rm
smmap/
-
0755
rm
smmap-5.0.0.dist-info/
-
0755
rm
snowballstemmer/
-
0755
rm
snowballstemmer-2.2.0.dist-info/
-
0755
rm
sqlalchemy/
-
0755
rm
sqlalchemy-1.3.24.dist-info/
-
0755
rm
ssa/
-
0755
rm
svgwrite/
-
0755
rm
svgwrite-1.4.3.dist-info/
-
0755
rm
tap/
-
0755
rm
tap_py-3.2.1.dist-info/
-
0755
rm
testfixtures/
-
0755
rm
testfixtures-7.1.0.dist-info/
-
0755
rm
toml/
-
0755
rm
toml-0.10.2.dist-info/
-
0755
rm
tomlkit/
-
0755
rm
tomlkit-0.11.8.dist-info/
-
0755
rm
typing_extensions-4.7.1.dist-info/
-
0755
rm
unshare-0.22.dist-info/
-
0755
rm
urllib3/
-
0755
rm
urllib3-2.0.4.dist-info/
-
0755
rm
vendors_api/
-
0755
rm
virtualenv/
-
0755
rm
virtualenv-20.21.1.dist-info/
-
0755
rm
wcwidth/
-
0755
rm
wcwidth-0.2.6.dist-info/
-
0755
rm
websiteisolation/
-
0755
rm
wmt/
-
0755
rm
wrapt/
-
0755
rm
wrapt-1.15.0.dist-info/
-
0755
rm
xray/
-
0755
rm
yaml/
-
0755
rm
yarl/
-
0755
rm
yarl-1.9.2.dist-info/
-
0755
rm
_distutils_hack/
-
0755
rm
_pytest/
-
0755
rm
_yaml/
-
0755
rm
__pycache__/
-
0755
rm
clcontrollib.py
53042
0644
edit
dl
rm
cldetectlib.py
18843
0644
edit
dl
rm
cldiaglib.py
49660
0644
edit
dl
rm
clhooklib.py
1296
0644
edit
dl
rm
cli_utils.py
1698
0644
edit
dl
rm
cllicenselib.py
11864
0644
edit
dl
rm
clsetuplib.py
5193
0644
edit
dl
rm
clsudo.py
23586
0644
edit
dl
rm
cl_proc_hidepid.py
4638
0644
edit
dl
rm
configparser.py
1546
0644
edit
dl
rm
ddt.py
12733
0644
edit
dl
rm
distutils-precedence.pth
151
0644
edit
dl
rm
docopt.py
19946
0644
edit
dl
rm
lveapi.py
23294
0644
edit
dl
rm
lvectllib.py
136479
0644
edit
dl
rm
lvestat.py
6997
0644
edit
dl
rm
mccabe.py
10654
0644
edit
dl
rm
pam.py
7556
0644
edit
dl
rm
pep8ext_naming.py
19052
0644
edit
dl
rm
py.py
263
0644
edit
dl
rm
pycodestyle.py
103501
0644
edit
dl
rm
pylve.cpython-311-x86_64-linux-gnu.so
29528
0755
edit
dl
rm
remove_ubc.py
5864
0755
edit
dl
rm
schema.py
30221
0644
edit
dl
rm
secureio.py
20037
0644
edit
dl
rm
simple_rpm.so
15544
0755
edit
dl
rm
six.py
34549
0644
edit
dl
rm
typing_extensions.py
111082
0644
edit
dl
rm
unshare.cpython-311-x86_64-linux-gnu.so
16704
0755
edit
dl
rm
_cffi_backend.cpython-311-x86_64-linux-gnu.so
274048
0755
edit
dl
rm
_lvdmap.cpython-311-x86_64-linux-gnu.so
18800
0755
edit
dl
rm
_pyrsistent_version.py
23
0644
edit
dl
rm
Edit:
/opt/cloudlinux/venv/lib/python3.11/site-packages/clsudo.py
(23586B)
# coding=utf-8 # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT import logging import os import pwd import grp import re import subprocess import tempfile from stat import S_IRUSR, S_IRGRP logger = logging.getLogger(__name__) class NoSuchUser(Exception): def __init__(self, user): Exception.__init__(self, f'No such user ({user})') class NoSuchGroup(Exception): def __init__(self, group): Exception.__init__(self, f'No such group ({group})') class UnableToReadFile(Exception): def __init__(self): Exception.__init__(self, 'Cannot read sudoers file') class UnableToWriteFile(Exception): def __init__(self): Exception.__init__(self, 'Cannot modify sudoers file') SUDOERS_FILE = '/etc/sudoers' # argv-restricted: bare command paths grant arbitrary argv as root (sudoers # semantics). ps/grep/service have no in-tree sudo caller -> pin to no args (""); # lvectl's only sudo caller is `lvectl lve-version`. ALIAS_LVECTL_CMDS = ['/bin/ps ""', '/bin/grep ""', '/sbin/service ""', "/usr/bin/getcontrolpaneluserspackages", "/usr/sbin/lvectl lve-version", "/usr/local/directadmin/plugins/new_lvemanager/admin/GetDomains", "/usr/share/l.v.e-manager/utils/cloudlinux-cli.py"] ALIAS_LVECTL_USER_CMDS = ["/usr/share/l.v.e-manager/utils/cloudlinux-cli-user.py"] # argv-restricted (same rationale as ALIAS_LVECTL_CMDS): no in-tree consumer # invokes these via sudo, so pin to no args ("") rather than arbitrary argv as root. ALIAS_CAGEFS_CMDS = ['/usr/sbin/cagefsctl ""', '/bin/ps ""', '/bin/grep ""', '/sbin/service ""'] # Detect a CAGEFS_CMDS / SELECTOR_CMDS alias definition regardless of the # (visudo-legal) whitespace around Cmnd_Alias, the alias name, and `=`. A # substring match on a single-space form misses a valid non-canonical line, so # the upgrade-rewrite would skip it and append a duplicate alias that # `visudo -c` then rejects. CAGEFS_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+CAGEFS_CMDS\s*=') SELECTOR_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+SELECTOR_CMDS\s*=') LVECTL_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+LVECTL_CMDS\s*=') # argv-restricted (same rationale as ALIAS_LVECTL_CMDS/ALIAS_CAGEFS_CMDS): the # only in-tree sudo caller named cl_selector.py runs /usr/sbin/cloudlinux-selector # (a different path) as the target user, not these paths as root; lveps runs from # the lve-stats root daemon without sudo; piniset has no caller (deprecated). No # consumer invokes these via the NOPASSWD-root grant, so pin to no args ("") # rather than arbitrary argv as root. ALIAS_SELECTOR_CMDS = ['/usr/bin/cl-selector ""', '/usr/bin/piniset ""', '/usr/sbin/lveps ""', '/usr/bin/selectorctl ""'] DEFAULTS_REQUIRETTY = 'Defaults:%s !requiretty' # Patterns for group GROUP_LVECTL_SELECTOR = '%%%s ALL=NOPASSWD: LVECTL_CMDS, SELECTOR_CMDS' GROUP_DEFAULTS_REQUIRETTY = 'Defaults:%%%s !requiretty' class Clsudo: """ Adds CloudLinux users to sudoers file """ # Fail-closed allowlist for the write destination. `sudoers_file` is a # public parameter that flows into `filepath`; the only path the product # ever writes is /etc/sudoers. Restricting the destination here ensures a # caller that forwarded an attacker-influenced path is refused rather than # dropping a sudoers file at an arbitrary location. Tests override this. _ALLOWED_SUDOERS_PATHS = (SUDOERS_FILE,) filepath = None sudoers_list = [] has_action = False has_group_action = False has_alias = False has_user_alias = False has_rights = False has_user_rights = False has_selector_alias = False has_selector_rights = False has_cagefs_alias = False has_cagefs_rights = False @staticmethod def add_user(user, sudoers_file=SUDOERS_FILE): """ Adds username to sudoers file (for lvemanager) """ logger.info("add_user: adding user '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) # Update command lists for lvemanager Clsudo.update_commands_list(sudoers_file) Clsudo._check_user(user) Clsudo._get_contents(user) if not Clsudo.has_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_CMDS = ' + ", ".join(ALIAS_LVECTL_CMDS)) if not Clsudo.has_user_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_USER_CMDS = ' + ", ".join(ALIAS_LVECTL_USER_CMDS)) if not Clsudo.has_selector_alias: Clsudo.sudoers_list.append('Cmnd_Alias SELECTOR_CMDS = ' + ", ".join(ALIAS_SELECTOR_CMDS)) if not Clsudo.has_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: LVECTL_CMDS') if not Clsudo.has_user_rights: Clsudo.sudoers_list.append(f'{user} ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS') if not Clsudo.has_selector_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: SELECTOR_CMDS') if not Clsudo.has_action: Clsudo.sudoers_list.append(DEFAULTS_REQUIRETTY % (user,)) Clsudo._write_contents() logger.info("add_user: successfully added user '%s' to '%s'", user, sudoers_file) @staticmethod def add_cagefs_user(user, sudoers_file=SUDOERS_FILE): """ Adds username to sudoers file (for cagefs) """ logger.info("add_cagefs_user: adding user '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) Clsudo.filepath = sudoers_file Clsudo._check_user(user) Clsudo._get_contents(user) if not Clsudo.has_cagefs_alias: Clsudo.sudoers_list.append('Cmnd_Alias CAGEFS_CMDS = ' + ", ".join(ALIAS_CAGEFS_CMDS)) if not Clsudo.has_cagefs_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: CAGEFS_CMDS') if not Clsudo.has_action: Clsudo.sudoers_list.append(DEFAULTS_REQUIRETTY % (user,)) Clsudo._write_contents() logger.info("add_cagefs_user: successfully added user '%s' to '%s'", user, sudoers_file) @staticmethod def add_lvemanager_group(group_name, sudoers_file=SUDOERS_FILE): """ Adds group to sudoers file, grants access to LVE Manager """ logger.info("add_lvemanager_group: adding group '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", group_name, sudoers_file, os.getuid(), os.getpid()) # Update command lists for lvemanager Clsudo.update_commands_list(sudoers_file) Clsudo._check_group(group_name) Clsudo._get_contents_group(group_name) if not Clsudo.has_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_CMDS = ' + ", ".join(ALIAS_LVECTL_CMDS)) if not Clsudo.has_selector_alias: Clsudo.sudoers_list.append('Cmnd_Alias SELECTOR_CMDS = ' + ", ".join(ALIAS_SELECTOR_CMDS)) if not Clsudo.has_action: Clsudo.sudoers_list.append(GROUP_LVECTL_SELECTOR % (group_name,)) if not Clsudo.has_group_action: Clsudo.sudoers_list.append(GROUP_DEFAULTS_REQUIRETTY % (group_name,)) # writes file Clsudo._write_contents() logger.info("add_lvemanager_group: successfully added group '%s' to '%s'", group_name, sudoers_file) @staticmethod def remove_user(user, sudoers_file=SUDOERS_FILE): """ Removes username from sudoers file """ logger.info("remove_user: removing user '%s' from sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) Clsudo.filepath = sudoers_file try: with open(Clsudo.filepath, encoding='utf-8') as f: Clsudo.sudoers_list = f.read().splitlines() idx = 0 removed = False while idx < len(Clsudo.sudoers_list): line = Clsudo.sudoers_list[idx] # Anchor matches at line start: a substring match would let # remove_user(<user>) also strip the rules of a different enrolled # admin whose name ends with <user> (remove_user('alice') must not # touch 'malice'). add_user writes both the plain `<user> ALL=NOPASSWD:` # rules and the `<user> ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS` rule # (line 94); match the `(ALL) ` form too so it is not orphaned (CLOS-4593 [28]). if (line.startswith(f'{user} ALL=NOPASSWD:') or line.startswith(f'{user} ALL=(ALL) NOPASSWD:') or line.startswith(DEFAULTS_REQUIRETTY % (user,))): logger.info("remove_user: removing sudoers rule for user '%s': %s", user, line) Clsudo.sudoers_list.remove(line) removed = True continue idx += 1 if removed: Clsudo._write_contents() logger.info("remove_user: successfully removed user '%s' from '%s'", user, sudoers_file) else: logger.info("remove_user: no rules found for user '%s' in '%s'", user, sudoers_file) except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def update_user(user, sudoers_file=SUDOERS_FILE): """ updates username in sudoers file :param user: username for caching :param sudoers_file: path to /etc/sudoers (only for tests) :return: None """ logger.info("update_user: updating user '%s' in sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) # Update command lists Clsudo.update_commands_list(sudoers_file) # For backward compatibility # Check user presence in system Clsudo._check_user(user) Clsudo._get_contents(user) @staticmethod def update_commands_list(sudoers_file=SUDOERS_FILE): """ Update command lists for lvemanager plugin If any required command absent in file, add it :param sudoers_file: path to /etc/sudoers :return: None """ # Read /etc/sudoers Clsudo.filepath = sudoers_file Clsudo.temp_dir = os.path.dirname(Clsudo.filepath) Clsudo._read_sudoers() # (regex, alias name, restricted command list) per upgradeable alias. # The anchored regexes tolerate any visudo-legal whitespace around # Cmnd_Alias / the alias name / `=`; _rewrite_alias collapses a # backslash line-continuation span and no-ops on an unrecognised form, # so an existing lax (or continuation) definition is upgraded in place # without duplication or corruption (same machinery as the CAGEFS_CMDS # upgrade in _get_contents). alias_specs = ((LVECTL_ALIAS_RE, 'LVECTL_CMDS', ALIAS_LVECTL_CMDS), (SELECTOR_ALIAS_RE, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS)) is_sudoer_change = False for idx, command_string in enumerate(Clsudo.sudoers_list): for alias_re, alias_name, alias_list in alias_specs: if alias_re.match(command_string): before = Clsudo.sudoers_list[idx] Clsudo._rewrite_alias(idx, alias_name, alias_list) if Clsudo.sudoers_list[idx] != before: is_sudoer_change = True break if is_sudoer_change: logger.info("update_commands_list: updating command aliases in '%s' " "(uid=%d, pid=%d)", sudoers_file, os.getuid(), os.getpid()) Clsudo._write_contents() @staticmethod def _check_user(user): """ Checks passwd database for username presence @param user: string """ try: pwd.getpwnam(user) except KeyError as e: raise NoSuchUser(user) from e @staticmethod def _check_group(group_name): """ Checks grp database for group_name presence @param group_name: string """ try: grp.getgrnam(group_name) except KeyError as e: raise NoSuchGroup(group_name) from e @staticmethod def _read_sudoers(): with open(Clsudo.filepath, encoding='utf-8') as f: Clsudo.sudoers_list = f.read().splitlines() @staticmethod def _rewrite_alias(idx, alias_name, alias_cmds): """Rewrite the existing `alias_name` Cmnd_Alias definition at sudoers_list[idx] to the single canonical argv-restricted line, in place. Shared by the CAGEFS_CMDS and SELECTOR_CMDS upgrade rewrites - both are lax bare-command aliases whose pre-fix on-disk form must be tightened to the argv-restricted `alias_cmds` join without duplicating or corrupting the definition. The definition may span several physical lines via visudo-legal backslash line-continuation (_read_sudoers splits on physical lines, so each continuation line is its own list element). Consume the whole span - the matched line plus every following line the preceding line continued onto - and replace it with one restricted line. Never-corrupt invariant: if the definition cannot be confidently and safely collapsed (a continuation whose trailing backslash runs off the end of the file - i.e. the file is already malformed), leave it exactly as-is. The worst case of the upgrade rewrite is a safe no-op (the file stays as valid as it was), never a corrupted sudoers file. The caller still sets the has_*_alias flag, so no duplicate definition is appended. """ lines = Clsudo.sudoers_list end = idx # Walk every physical line the preceding line continued onto (trailing # backslash). A dangling continuation (last line of the span ends in a # backslash with no successor) is an already-malformed form we do not # recognise -> bail out, leaving the definition untouched. while lines[end].endswith('\\'): if end + 1 >= len(lines): return end += 1 restricted = f'Cmnd_Alias {alias_name} = ' + ", ".join(alias_cmds) if end == idx and lines[idx].strip() == restricted: return # Collapse [idx, end] (matched line + its continuation lines) to the # single restricted line. Only elements after idx are removed, so the # enumerate() in the caller resumes correctly at the next real line. lines[idx:end + 1] = [restricted] @staticmethod def _get_contents(user): """ Reads file into list of strings @param user: string """ # Clear all status flags Clsudo.has_action = False Clsudo.has_group_action = False Clsudo.has_alias = False Clsudo.has_user_alias = False Clsudo.has_rights = False Clsudo.has_user_rights = False Clsudo.has_selector_alias = False Clsudo.has_selector_rights = False Clsudo.has_cagefs_alias = False Clsudo.has_cagefs_rights = False require_tty_pattern = re.compile(rf'Defaults:\s*{user}\s*!requiretty') try: # Read sudoers file Clsudo._read_sudoers() for idx, command_string in enumerate(Clsudo.sudoers_list): if "Cmnd_Alias LVECTL_CMDS" in command_string: Clsudo.has_alias = True continue if "Cmnd_Alias LVECTL_USER_CMDS" in command_string: Clsudo.has_user_alias = True continue if CAGEFS_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax CAGEFS_CMDS definition # (bare command paths grant arbitrary argv as root) to the # argv-restricted form in place. Matched whitespace-tolerantly # and continuation-aware so a valid non-canonical or multi-line # definition is rewritten, not duplicated or corrupted. Clsudo._rewrite_alias(idx, 'CAGEFS_CMDS', ALIAS_CAGEFS_CMDS) Clsudo.has_cagefs_alias = True continue # Anchor to the start of the line so we don't false-positive on # a longer user whose name ends with `user` — e.g. searching for # `admin ALL=NOPASSWD: CAGEFS_CMDS` must not match a pre-existing # `newadmin ALL=NOPASSWD: CAGEFS_CMDS` line, or add_cagefs_user # silently skips granting the rights and DA admins land without # the sudo entry the hook is supposed to install. if command_string.lstrip().startswith(f"{user} ALL=NOPASSWD: LVECTL_CMDS"): Clsudo.has_rights = True continue if command_string.lstrip().startswith(f"{user} ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS"): Clsudo.has_user_rights = True continue if command_string.lstrip().startswith(f"{user} ALL=NOPASSWD: CAGEFS_CMDS"): Clsudo.has_cagefs_rights = True continue if "requiretty" in command_string: pattern_match = require_tty_pattern.search(command_string) if pattern_match: Clsudo.has_action = True continue if SELECTOR_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax SELECTOR_CMDS definition # (bare command paths grant arbitrary argv as root) to the # argv-restricted form in place, same whitespace-tolerant, # continuation-aware, never-corrupt rewrite as CAGEFS_CMDS. Clsudo._rewrite_alias(idx, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS) Clsudo.has_selector_alias = True continue if command_string.lstrip().startswith(f"{user} ALL=NOPASSWD: SELECTOR_CMDS"): Clsudo.has_selector_rights = True continue except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def _get_contents_group(group_name): """ Reads file into list of strings @param group_name: string """ # Clear all status flags Clsudo.has_action = False Clsudo.has_group_action = False Clsudo.has_alias = False Clsudo.has_rights = False Clsudo.has_selector_alias = False Clsudo.has_selector_rights = False Clsudo.has_cagefs_alias = False Clsudo.has_cagefs_rights = False group_prefix = f"%{group_name}" group_action = f"Defaults:%{group_name}" group_pattern = re.compile(rf'{group_name}\s*ALL=NOPASSWD:\s*LVECTL_CMDS,\s*SELECTOR_CMDS') try: # Read sudoers file Clsudo._read_sudoers() for idx, command_string in enumerate(Clsudo.sudoers_list): if SELECTOR_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax SELECTOR_CMDS definition # to the argv-restricted form in place (see _get_contents). Clsudo._rewrite_alias(idx, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS) Clsudo.has_selector_alias = True continue if "Cmnd_Alias LVECTL_CMDS" in command_string: Clsudo.has_alias = True continue if CAGEFS_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax CAGEFS_CMDS definition # to the argv-restricted form in place (see _get_contents). Clsudo._rewrite_alias(idx, 'CAGEFS_CMDS', ALIAS_CAGEFS_CMDS) Clsudo.has_cagefs_alias = True continue if command_string.startswith(group_prefix): pattern_match = group_pattern.search(command_string) if pattern_match: Clsudo.has_action = True if command_string.startswith(group_action): Clsudo.has_group_action = True except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def _write_contents(): """ Writes data to temporary file then checks it and rewrites sudoers file """ # Fail-closed destination allowlist: visudo -c (below) validates sudoers # SYNTAX, not LOCATION, so the write target itself must be constrained. # Refuse any destination not on the approved list before creating or # renaming any file. allowed = {os.path.realpath(p) for p in Clsudo._ALLOWED_SUDOERS_PATHS} if os.path.realpath(Clsudo.filepath) not in allowed: logger.warning("_write_contents: refusing to write sudoers to " "disallowed destination '%s' (uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) raise UnableToWriteFile() try: temp_dir = os.path.dirname(Clsudo.filepath) temp_prefix = 'lve_sudoers_' fd, temp_path = tempfile.mkstemp(prefix=temp_prefix, dir=temp_dir) fo = os.fdopen(fd, 'w') fo.write('\n'.join(Clsudo.sudoers_list) + '\n') fo.close() mask = S_IRUSR | S_IRGRP os.chmod(temp_path, mask) if not Clsudo._is_file_valid(temp_path): raise IOError except (IOError, OSError) as e: logger.warning("_write_contents: failed to write sudoers file '%s' " "(uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) try: if os.path.exists(temp_path): os.unlink(temp_path) except Exception: pass raise UnableToWriteFile() from e try: os.rename(temp_path, Clsudo.filepath) except OSError as e: logger.warning("_write_contents: failed to rename temp file to '%s' " "(uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) raise UnableToWriteFile() from e @staticmethod def _is_file_valid(filename): cmd = [ '/usr/sbin/visudo', '-c', '-f', filename ] with subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as proc: proc.communicate() if proc.returncode != 0: return False return True
Save
cmd:
run