mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-07 22:02:50 +00:00
Don't create world-readable module and tempfiles without explicit user permission
This commit is contained in:
@@ -192,7 +192,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _make_tmp_path(self):
|
||||
def _make_tmp_path(self, remote_user):
|
||||
'''
|
||||
Create and return a temporary path on a remote box.
|
||||
'''
|
||||
@@ -200,12 +200,10 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
|
||||
use_system_tmp = False
|
||||
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
if self._play_context.become and self._play_context.become_user not in ('root', remote_user):
|
||||
use_system_tmp = True
|
||||
|
||||
tmp_mode = None
|
||||
if self._play_context.remote_user != 'root' or self._play_context.become and self._play_context.become_user != 'root':
|
||||
tmp_mode = 0o755
|
||||
tmp_mode = 0o700
|
||||
|
||||
cmd = self._connection._shell.mkdtemp(basefile, use_system_tmp, tmp_mode)
|
||||
result = self._low_level_execute_command(cmd, sudoable=False)
|
||||
@@ -255,6 +253,10 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
# If ssh breaks we could leave tmp directories out on the remote system.
|
||||
self._low_level_execute_command(cmd, sudoable=False)
|
||||
|
||||
def _transfer_file(self, local_path, remote_path):
|
||||
self._connection.put_file(local_path, remote_path)
|
||||
return remote_path
|
||||
|
||||
def _transfer_data(self, remote_path, data):
|
||||
'''
|
||||
Copies the module data out to the temporary module path.
|
||||
@@ -269,25 +271,85 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
data = to_bytes(data, errors='strict')
|
||||
afo.write(data)
|
||||
except Exception as e:
|
||||
#raise AnsibleError("failure encoding into utf-8: %s" % str(e))
|
||||
raise AnsibleError("failure writing module data to temporary file for transfer: %s" % str(e))
|
||||
|
||||
afo.flush()
|
||||
afo.close()
|
||||
|
||||
try:
|
||||
self._connection.put_file(afile, remote_path)
|
||||
self._transfer_file(afile, remote_path)
|
||||
finally:
|
||||
os.unlink(afile)
|
||||
|
||||
return remote_path
|
||||
|
||||
def _remote_chmod(self, mode, path, sudoable=False):
|
||||
def _fixup_perms(self, remote_path, remote_user, execute=False, recursive=True):
|
||||
"""
|
||||
If the become_user is unprivileged and different from the
|
||||
remote_user then we need to make the files we've uploaded readable by them.
|
||||
"""
|
||||
if remote_path is None:
|
||||
# Sometimes code calls us naively -- it has a var which could
|
||||
# contain a path to a tmp dir but doesn't know if it needs to
|
||||
# exist or not. If there's no path, then there's no need for us
|
||||
# to do work
|
||||
self._display.debug('_fixup_perms called with remote_path==None. Sure this is correct?')
|
||||
return remote_path
|
||||
|
||||
if self._play_context.become and self._play_context.become_user not in ('root', remote_user):
|
||||
# Unprivileged user that's different than the ssh user. Let's get
|
||||
# to work!
|
||||
if remote_user == 'root':
|
||||
# SSh'ing as root, therefore we can chown
|
||||
self._remote_chown(remote_path, self._play_context.become_user, recursive=recursive)
|
||||
if execute:
|
||||
# root can read things that don't have read bit but can't
|
||||
# execute them.
|
||||
self._remote_chmod('u+x', remote_path, recursive=recursive)
|
||||
else:
|
||||
if execute:
|
||||
mode = 'rx'
|
||||
else:
|
||||
mode = 'rX'
|
||||
# Try to use fs acls to solve this problem
|
||||
res = self._remote_set_user_facl(remote_path, self._play_context.become_user, mode, recursive=recursive, sudoable=False)
|
||||
if res['rc'] != 0:
|
||||
if C.ALLOW_WORLD_READABLE_TMPFILES:
|
||||
# fs acls failed -- do things this insecure way only
|
||||
# if the user opted in in the config file
|
||||
self._display.warning('Using world-readable permissions for temporary files Ansible needs to create when becoming an unprivileged user which may be insecure. For information on securing this, see https://docs.ansible.com/ansible/become.html#becoming-an-unprivileged-user')
|
||||
self._remote_chmod('a+%s' % mode, remote_path, recursive=recursive)
|
||||
else:
|
||||
raise AnsibleError('Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user. For information on working around this, see https://docs.ansible.com/ansible/become.html#becoming-an-unprivileged-user')
|
||||
elif execute:
|
||||
# Can't depend on the file being transferred with execute
|
||||
# permissions. Only need user perms because no become was
|
||||
# used here
|
||||
self._remote_chmod('u+x', remote_path, recursive=recursive)
|
||||
|
||||
return remote_path
|
||||
|
||||
def _remote_chmod(self, mode, path, recursive=True, sudoable=False):
|
||||
'''
|
||||
Issue a remote chmod command
|
||||
'''
|
||||
cmd = self._connection._shell.chmod(mode, path, recursive=recursive)
|
||||
res = self._low_level_execute_command(cmd, sudoable=sudoable)
|
||||
return res
|
||||
|
||||
cmd = self._connection._shell.chmod(mode, path)
|
||||
def _remote_chown(self, path, user, group=None, recursive=True, sudoable=False):
|
||||
'''
|
||||
Issue a remote chown command
|
||||
'''
|
||||
cmd = self._connection._shell.chown(path, user, group, recursive=recursive)
|
||||
res = self._low_level_execute_command(cmd, sudoable=sudoable)
|
||||
return res
|
||||
|
||||
def _remote_set_user_facl(self, path, user, mode, recursive=True, sudoable=False):
|
||||
'''
|
||||
Issue a remote call to setfacl
|
||||
'''
|
||||
cmd = self._connection._shell.set_user_facl(path, user, mode, recursive=recursive)
|
||||
res = self._low_level_execute_command(cmd, sudoable=sudoable)
|
||||
return res
|
||||
|
||||
@@ -417,6 +479,9 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
else:
|
||||
module_args['_ansible_check_mode'] = False
|
||||
|
||||
# Get the connection user for permission checks
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
|
||||
# set no log in the module arguments, if required
|
||||
module_args['_ansible_no_log'] = self._play_context.no_log or C.DEFAULT_NO_TARGET_SYSLOG
|
||||
|
||||
@@ -437,7 +502,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
remote_module_path = None
|
||||
args_file_path = None
|
||||
if not tmp and self._late_needs_tmp_path(tmp, module_style):
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
if tmp:
|
||||
remote_module_filename = self._connection._shell.get_remote_filename(module_name)
|
||||
@@ -462,11 +527,9 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||
|
||||
environment_string = self._compute_environment_string()
|
||||
|
||||
if tmp and "tmp" in tmp and self._play_context.become and self._play_context.become_user != 'root':
|
||||
# deal with possible umask issues once sudo'ed to other user
|
||||
self._remote_chmod('a+r', remote_module_path)
|
||||
if args_file_path is not None:
|
||||
self._remote_chmod('a+r', args_file_path)
|
||||
# Fix permissions of the tmp path and tmp files. This should be
|
||||
# called after all files have been transferred.
|
||||
self._fixup_perms(tmp, remote_user, recursive=True)
|
||||
|
||||
cmd = ""
|
||||
in_data = None
|
||||
|
||||
@@ -98,8 +98,9 @@ class ActionModule(ActionBase):
|
||||
return result
|
||||
|
||||
cleanup_remote_tmp = False
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
cleanup_remote_tmp = True
|
||||
|
||||
if boolean(remote_src):
|
||||
@@ -146,16 +147,15 @@ class ActionModule(ActionBase):
|
||||
)
|
||||
|
||||
if path_checksum != dest_stat['checksum']:
|
||||
resultant = file(path).read()
|
||||
|
||||
if self._play_context.diff:
|
||||
diff = self._get_diff_data(dest, path, task_vars)
|
||||
|
||||
xfered = self._transfer_data('src', resultant)
|
||||
remote_path = self._connection._shell.join_path(tmp, 'src')
|
||||
xfered = self._transfer_file(path, remote_path)
|
||||
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
self._remote_chmod('a+r', xfered)
|
||||
self._fixup_perms(tmp, remote_user, recursive=True)
|
||||
|
||||
new_module_args.update( dict( src=xfered,))
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@ class ActionModule(ActionBase):
|
||||
result['msg'] = 'check mode not supported for this module'
|
||||
return result
|
||||
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
module_name = self._task.action
|
||||
async_module_path = self._connection._shell.join_path(tmp, 'async_wrapper')
|
||||
@@ -54,15 +55,25 @@ class ActionModule(ActionBase):
|
||||
# configure, upload, and chmod the target module
|
||||
(module_style, shebang, module_data) = self._configure_module(module_name=module_name, module_args=module_args, task_vars=task_vars)
|
||||
self._transfer_data(remote_module_path, module_data)
|
||||
self._remote_chmod('a+rx', remote_module_path)
|
||||
|
||||
# configure, upload, and chmod the async_wrapper module
|
||||
(async_module_style, shebang, async_module_data) = self._configure_module(module_name='async_wrapper', module_args=dict(), task_vars=task_vars)
|
||||
self._transfer_data(async_module_path, async_module_data)
|
||||
self._remote_chmod('a+rx', async_module_path)
|
||||
|
||||
argsfile = self._transfer_data(self._connection._shell.join_path(tmp, 'arguments'), json.dumps(module_args))
|
||||
|
||||
self._fixup_perms(tmp, remote_user, execute=True, recursive=True)
|
||||
# Only the following two files need to be executable but we'd have to
|
||||
# make three remote calls if we wanted to just set them executable.
|
||||
# There's not really a problem with marking too many of the temp files
|
||||
# executable so we go ahead and mark them all as executable in the
|
||||
# line above (the line above is needed in any case [although
|
||||
# execute=False is okay if we uncomment the lines below] so that all
|
||||
# the files are readable in case the remote_user and become_user are
|
||||
# different and both unprivileged)
|
||||
#self._fixup_perms(remote_module_path, remote_user, execute=True, recursive=False)
|
||||
#self._fixup_perms(async_module_path, remote_user, execute=True, recursive=False)
|
||||
|
||||
async_limit = self._task.async
|
||||
async_jid = str(random.randint(0, 999999999999))
|
||||
|
||||
|
||||
@@ -141,9 +141,10 @@ class ActionModule(ActionBase):
|
||||
delete_remote_tmp = (len(source_files) == 1)
|
||||
|
||||
# If this is a recursive action create a tmp path that we can share as the _exec_module create is too late.
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
if not delete_remote_tmp:
|
||||
if tmp is None or "-tmp-" not in tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
# expand any user home dir specifier
|
||||
dest = self._remote_expand_user(dest)
|
||||
@@ -196,7 +197,7 @@ class ActionModule(ActionBase):
|
||||
# If this is recursive we already have a tmp path.
|
||||
if delete_remote_tmp:
|
||||
if tmp is None or "-tmp-" not in tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
if self._play_context.diff and not raw:
|
||||
diffs.append(self._get_diff_data(dest_file, source_full, task_vars))
|
||||
@@ -211,16 +212,15 @@ class ActionModule(ActionBase):
|
||||
tmp_src = self._connection._shell.join_path(tmp, 'source')
|
||||
|
||||
if not raw:
|
||||
self._connection.put_file(source_full, tmp_src)
|
||||
self._transfer_file(source_full, tmp_src)
|
||||
else:
|
||||
self._connection.put_file(source_full, dest_file)
|
||||
self._transfer_file(source_full, dest_file)
|
||||
|
||||
# We have copied the file remotely and no longer require our content_tempfile
|
||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
self._remote_chmod('a+r', tmp_src)
|
||||
self._fixup_perms(tmp, remote_user, recursive=True)
|
||||
|
||||
if raw:
|
||||
# Continue to next iteration if raw is defined.
|
||||
|
||||
@@ -34,6 +34,7 @@ class ActionModule(ActionBase):
|
||||
|
||||
src = self._task.args.get('src', None)
|
||||
remote_src = boolean(self._task.args.get('remote_src', 'no'))
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
|
||||
if src is None:
|
||||
result['failed'] = True
|
||||
@@ -52,14 +53,12 @@ class ActionModule(ActionBase):
|
||||
|
||||
# create the remote tmp dir if needed, and put the source file there
|
||||
if tmp is None or "-tmp-" not in tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
tmp_src = self._connection._shell.join_path(tmp, os.path.basename(src))
|
||||
self._connection.put_file(src, tmp_src)
|
||||
self._transfer_file(src, tmp_src)
|
||||
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
if not self._play_context.check_mode:
|
||||
self._remote_chmod('a+r', tmp_src)
|
||||
self._fixup_perms(tmp, remote_user, recursive=True)
|
||||
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
|
||||
@@ -38,8 +38,9 @@ class ActionModule(ActionBase):
|
||||
result['msg'] = 'check mode not supported for this module'
|
||||
return result
|
||||
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
creates = self._task.args.get('creates')
|
||||
if creates:
|
||||
@@ -76,16 +77,11 @@ class ActionModule(ActionBase):
|
||||
|
||||
# transfer the file to a remote tmp location
|
||||
tmp_src = self._connection._shell.join_path(tmp, os.path.basename(source))
|
||||
self._connection.put_file(source, tmp_src)
|
||||
self._transfer_file(source, tmp_src)
|
||||
|
||||
sudoable = True
|
||||
# set file permissions, more permissive when the copy is done as a different user
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
chmod_mode = 'a+rx'
|
||||
sudoable = False
|
||||
else:
|
||||
chmod_mode = '+rx'
|
||||
self._remote_chmod(chmod_mode, tmp_src, sudoable=sudoable)
|
||||
self._fixup_perms(tmp, remote_user, execute=True, recursive=True)
|
||||
|
||||
# add preparation steps to one ssh roundtrip executing the script
|
||||
env_string = self._compute_environment_string()
|
||||
|
||||
@@ -138,8 +138,9 @@ class ActionModule(ActionBase):
|
||||
return result
|
||||
|
||||
cleanup_remote_tmp = False
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
cleanup_remote_tmp = True
|
||||
|
||||
local_checksum = checksum_s(resultant)
|
||||
@@ -163,8 +164,7 @@ class ActionModule(ActionBase):
|
||||
xfered = self._transfer_data(self._connection._shell.join_path(tmp, 'source'), resultant)
|
||||
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
self._remote_chmod('a+r', xfered)
|
||||
self._fixup_perms(tmp, remote_user, recursive=True)
|
||||
|
||||
# run the copy module
|
||||
new_module_args.update(
|
||||
|
||||
@@ -45,8 +45,9 @@ class ActionModule(ActionBase):
|
||||
result['msg'] = "src (or content) and dest are required"
|
||||
return result
|
||||
|
||||
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
tmp = self._make_tmp_path(remote_user)
|
||||
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
@@ -80,17 +81,15 @@ class ActionModule(ActionBase):
|
||||
|
||||
if copy:
|
||||
# transfer the file to a remote tmp location
|
||||
tmp_src = tmp + 'source'
|
||||
self._connection.put_file(source, tmp_src)
|
||||
tmp_src = self._connection._shell.join_path(tmp, 'source')
|
||||
self._transfer_file(source, tmp_src)
|
||||
|
||||
# handle diff mode client side
|
||||
# handle check mode client side
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if copy:
|
||||
if self._play_context.become and self._play_context.become_user != 'root':
|
||||
if not self._play_context.check_mode:
|
||||
self._remote_chmod('a+r', tmp_src)
|
||||
|
||||
if copy:
|
||||
# fix file permissions when the copy is done as a different user
|
||||
self._fixup_perms(tmp, remote_user, recursive=True)
|
||||
# Build temporary module_args.
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
|
||||
@@ -52,9 +52,40 @@ class ShellBase(object):
|
||||
def path_has_trailing_slash(self, path):
|
||||
return path.endswith('/')
|
||||
|
||||
def chmod(self, mode, path):
|
||||
def chmod(self, mode, path, recursive=True):
|
||||
path = pipes.quote(path)
|
||||
return 'chmod %s %s' % (mode, path)
|
||||
cmd = ['chmod', mode, path]
|
||||
if recursive:
|
||||
cmd.append('-R')
|
||||
return ' '.join(cmd)
|
||||
|
||||
def chown(self, path, user, group=None, recursive=True):
|
||||
path = pipes.quote(path)
|
||||
user = pipes.quote(user)
|
||||
|
||||
if group is None:
|
||||
cmd = ['chown', user, path]
|
||||
else:
|
||||
group = pipes.quote(group)
|
||||
cmd = ['chown', '%s:%s' % (user, group), path]
|
||||
|
||||
if recursive:
|
||||
cmd.append('-R')
|
||||
|
||||
return ' '.join(cmd)
|
||||
|
||||
def set_user_facl(self, path, user, mode, recursive=True):
|
||||
"""Only sets acls for users as that's really all we need"""
|
||||
path = pipes.quote(path)
|
||||
mode = pipes.quote(mode)
|
||||
user = pipes.quote(user)
|
||||
|
||||
cmd = ['setfacl']
|
||||
if recursive:
|
||||
cmd.append('-R')
|
||||
cmd.extend(('-m', 'u:%s:%s %s' % (user, mode, path)))
|
||||
|
||||
return ' '.join(cmd)
|
||||
|
||||
def remove(self, path, recurse=False):
|
||||
path = pipes.quote(path)
|
||||
|
||||
Reference in New Issue
Block a user