"""Fixperms class for cPanel"""
from shlex import quote, join as cmd_join
from subprocess import CalledProcessError, check_call
from stat import S_ISLNK, S_ISREG, S_ISDIR
from fixperms_base import PermMap
from fixperms_cli import Args
from fixperms_ids import IDCache
class CpanelPermMap(PermMap):
"""Fixperms class for cPanel"""
def __init__(self, ids: IDCache, args: Args, user: str):
all_docroots=rads.UserData(user).all_roots,
docroot_chown=(user, 'nobody'),
self.is_shared = rads.IMH_ROLE == 'shared'
# always skip ~/etc and ~/mail in the main os.walk - that's what
self.skip.add(os.path.join(self.homedir, 'mail'))
self.skip.add(os.path.join(self.homedir, 'etc'))
# pylint: disable=duplicate-code
# Order these rules more specific to less specific regex.
uid, gid = self.uid, self.gid
# sensitive passwords: ~/.accesshash, ~/.pgpass, ~/.my.cnf
r"\/\.(?:accesshash|pgpass|my\.cnf)$", (0o600, None), (uid, gid)
# ~/.imh/nginx - ngxconf & cache manager files
self.add_rule(r"\/\.imh\/nginx(?:$|\/)", (0o664, 0o775), (uid, gid))
# ~/.imh directory and contents
self.add_rule(r"\/\.imh(?:$|\/)", (0o644, 0o755), (0, 0))
# ~/.ssh directory and contents
self.add_rule(r"\/\.ssh(?:$|\/)", (0o600, 0o700), (uid, gid))
self.add_rule(r"\/\.pki(?:$|\/)", (None, 0o740), (uid, gid))
self.add_rule(r"\/.*\.(?:pl|cgi)$", (0o755, None), (uid, gid))
self.add_rule("$", (None, 0o711), (uid, gid))
# restrict access to sensitive CMS config files
r"\/.+\/(?:(?:wp-config|conf|[cC]onfig|[cC]onfiguration|"
r"LocalSettings|settings)(?:\.inc)?\.php|"
r"local\.xml|mt-config\.cgi)$",
# contents of homedir which do not match a previous regex
self.add_rule(r"\/", (0o644, 0o755), (uid, gid))
# full path to symlink sources which are safe
os.path.join(self.homedir, '.cphorde/meta/latest'),
os.path.join(self.homedir, 'www'),
# regex for symlink sources which are safe
fr'(?:{self.home_re}\/(?:etc|mail|logs)\/)',
r'(?:.*\/\.ea-php-cli\.cache$)',
self.safe_link_src_re = re.compile('|'.join(safe_link_src_re))
# full path to symlink destinations which are safe
os.path.join('/usr/local/apache/domlogs', self.user),
os.path.join('/etc/apache2/logs/domlogs', self.user),
os.path.join('/var/log/apache2/domlogs', self.user),
'/home/shrusr/SharedHtDocsDir',
'/var/lib/mysql/mysql.sock',
'/var/run/postgres/.s.PGSQL.5432',
'/run/postgres/.s.PGSQL.5432',
'/usr/local/cpanel/base/frontend/paper_lantern/styled/retro',
def link_unsafe(self, path: str) -> bool:
"""Determine if a symlink is "unsafe" for a shared server"""
if path in self.safe_link_src:
if os.path.realpath(path) in self.safe_link_dest:
if self.safe_link_src_re.match(path):
bad_link = f'{quote(path)} -> {quote(os.readlink(path))}'
self.bad_links.append(bad_link)
self.log.warning('Potentially malicious symlink detected: %s', bad_link)
"""Run /scripts/mailperm"""
self.mailperm_fix('mail', self.gid)
self.mailperm_fix('etc', self.ids.getgrnam('mail').gr_gid)
'/usr/local/cpanel/scripts/mailperm',
self.log.debug('Running: %s', cmd_join(cmd_args))
except (CalledProcessError, OSError):
self.log.error('Error running: %s', cmd_join(cmd_args))
def fixperms(self) -> None:
def check_path(self, stat: os.stat_result, path: str):
if S_ISLNK(stat.st_mode) and self.link_unsafe(path):
super().check_path(stat, path)
def mailperm_fix(self, subdir: str, dir_gid: int):
"""Fix permissions not caught by cPanel's mailperm script"""
top_dir = os.path.join(self.homedir, subdir)
mail_gid = self.ids.getgrnam('mail').gr_gid
dir_gids = {self.gid, dir_gid}
for stat, path in self.walk(top_dir, ignore_skips=True):
if S_ISREG(stat.st_mode): # path is a regular file
if stat.st_gid in (self.gid, mail_gid):
if self.uid != stat.st_uid and stat.st_nlink > 1:
self.hard_links.add(path, stat, (self.uid, gid), None)
self.lchown(path, stat, self.uid, gid)
elif S_ISDIR(stat.st_mode): # path is a directory
# for each directory with a group not set to the user or mail
# chgrp to user:mail if ~/etc, user:user if ~/mail
if stat.st_gid in dir_gids:
self.lchown(path, stat, self.uid, -1)
self.lchown(path, stat, self.uid, dir_gid)
elif S_ISLNK(stat.st_mode): # path is a symlink
if self.link_unsafe(path):
self.lchown(path, stat, self.uid, self.gid)
else: # path is socket/device/fifo/etc
self.log.warning("Skipping unexpected path type at %s", path)
"""Send an email to str@imhadmin.net if malicious symlinks are found"""
bad_links = "\n".join(self.bad_links)
"Fixperms detected and removed the following symlinks. While these "
"symlinks have been removed from the account in question the "
"account requires further investigation"
self.log.info("An STR will now be sent for review by T2S staff")
to_addr='str@imhadmin.net',
subject=f'AUTO STR: bad symlinks on {self.user}',
body=f'{top}\n\n{bad_links}',
"Failed to send STR. An escalation must be sent to an",
"available T2S. Include the following information\n\n",