mirror of
https://github.com/ansible/ansible.git
synced 2026-08-04 08:03:41 +02:00
* Fix installing roles containing symlinks
Fix sanitizing tarfile symlinks relative to the link directory instead of the archive
For example:
role
├── handlers
│ └── utils.yml -> ../tasks/utils/suite.yml
The link ../tasks/utils/suite.yml will resolve to a path outside of the link's directory, but within the role
role/handlers/../tasks/utils/suite.yml
the resolved path relative to the role is tasks/utils/suite.yml, but if the symlink is set to that value, tarfile would extract it from role/handlers/tasks/utils/suite.yml
* Replace overly forgiving test case with tests for a symlink in a subdirectory of the archive and a symlink in the archive dir when these are not equivalent.
* Build test case from role files to make it easier to add test cases
Fixes #82702
Fixes #81965
Fixes #82051
(cherry picked from commit e84240db84)
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
bugfixes:
|
bugfixes:
|
||||||
- ansible-galaxy role install - normalize tarfile paths and symlinks using ``ansible.utils.path.unfrackpath`` and consider them valid as long as the realpath is in the tarfile's role directory (https://github.com/ansible/ansible/issues/81965).
|
- ansible-galaxy role install - normalize tarfile paths and symlinks using ``ansible.utils.path.unfrackpath`` and consider them valid as long as the realpath is in the tarfile's role directory (https://github.com/ansible/ansible/issues/81965).
|
||||||
|
- ansible-galaxy role install - fix symlinks (https://github.com/ansible/ansible/issues/82702, https://github.com/ansible/ansible/issues/81965).
|
||||||
|
|||||||
+10
-12
@@ -387,6 +387,8 @@ class GalaxyRole(object):
|
|||||||
else:
|
else:
|
||||||
os.makedirs(self.path)
|
os.makedirs(self.path)
|
||||||
|
|
||||||
|
resolved_archive = unfrackpath(archive_parent_dir, follow=False)
|
||||||
|
|
||||||
# We strip off any higher-level directories for all of the files
|
# We strip off any higher-level directories for all of the files
|
||||||
# contained within the tar file here. The default is 'github_repo-target'.
|
# contained within the tar file here. The default is 'github_repo-target'.
|
||||||
# Gerrit instances, on the other hand, does not have a parent directory at all.
|
# Gerrit instances, on the other hand, does not have a parent directory at all.
|
||||||
@@ -401,33 +403,29 @@ class GalaxyRole(object):
|
|||||||
if not (attr_value := getattr(member, attr, None)):
|
if not (attr_value := getattr(member, attr, None)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if attr_value.startswith(os.sep) and not is_subpath(attr_value, archive_parent_dir):
|
|
||||||
err = f"Invalid {attr} for tarfile member: path {attr_value} is not a subpath of the role {archive_parent_dir}"
|
|
||||||
raise AnsibleError(err)
|
|
||||||
|
|
||||||
if attr == 'linkname':
|
if attr == 'linkname':
|
||||||
# Symlinks are relative to the link
|
# Symlinks are relative to the link
|
||||||
relative_to_archive_dir = os.path.dirname(getattr(member, 'name', ''))
|
relative_to = os.path.dirname(getattr(member, 'name', ''))
|
||||||
archive_dir_path = os.path.join(archive_parent_dir, relative_to_archive_dir, attr_value)
|
|
||||||
else:
|
else:
|
||||||
# Normalize paths that start with the archive dir
|
# Normalize paths that start with the archive dir
|
||||||
attr_value = attr_value.replace(archive_parent_dir, "", 1)
|
attr_value = attr_value.replace(archive_parent_dir, "", 1)
|
||||||
attr_value = os.path.join(*attr_value.split(os.sep)) # remove leading os.sep
|
attr_value = os.path.join(*attr_value.split(os.sep)) # remove leading os.sep
|
||||||
archive_dir_path = os.path.join(archive_parent_dir, attr_value)
|
relative_to = ''
|
||||||
|
|
||||||
resolved_archive = unfrackpath(archive_parent_dir)
|
full_path = os.path.join(resolved_archive, relative_to, attr_value)
|
||||||
resolved_path = unfrackpath(archive_dir_path)
|
if not is_subpath(full_path, resolved_archive, real=True):
|
||||||
if not is_subpath(resolved_path, resolved_archive):
|
err = f"Invalid {attr} for tarfile member: path {full_path} is not a subpath of the role {resolved_archive}"
|
||||||
err = f"Invalid {attr} for tarfile member: path {resolved_path} is not a subpath of the role {resolved_archive}"
|
|
||||||
raise AnsibleError(err)
|
raise AnsibleError(err)
|
||||||
|
|
||||||
relative_path = os.path.join(*resolved_path.replace(resolved_archive, "", 1).split(os.sep)) or '.'
|
relative_path_dir = os.path.join(resolved_archive, relative_to)
|
||||||
|
relative_path = os.path.join(*full_path.replace(relative_path_dir, "", 1).split(os.sep))
|
||||||
setattr(member, attr, relative_path)
|
setattr(member, attr, relative_path)
|
||||||
|
|
||||||
if _check_working_data_filter():
|
if _check_working_data_filter():
|
||||||
# deprecated: description='extract fallback without filter' python_version='3.11'
|
# deprecated: description='extract fallback without filter' python_version='3.11'
|
||||||
role_tar_file.extract(member, to_native(self.path), filter='data') # type: ignore[call-arg]
|
role_tar_file.extract(member, to_native(self.path), filter='data') # type: ignore[call-arg]
|
||||||
else:
|
else:
|
||||||
|
# Remove along with manual path filter once Python 3.12 is minimum supported version
|
||||||
role_tar_file.extract(member, to_native(self.path))
|
role_tar_file.extract(member, to_native(self.path))
|
||||||
|
|
||||||
# write out the install info file for later use
|
# write out the install info file for later use
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
common_vars/subdir/group0/main.yml
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../tasks/utils/suite.yml
|
||||||
@@ -1,78 +1,38 @@
|
|||||||
- name: create test directories
|
- delegate_to: localhost
|
||||||
file:
|
block:
|
||||||
path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}'
|
- name: Create archive
|
||||||
state: directory
|
command: "tar -cf safe-symlinks.tar {{ role_path }}/files/safe-symlinks"
|
||||||
loop:
|
args:
|
||||||
- source
|
chdir: "{{ remote_tmp_dir }}"
|
||||||
- target
|
|
||||||
- roles
|
|
||||||
|
|
||||||
- name: create subdir in the role content to test relative symlinks
|
- name: Install role successfully
|
||||||
file:
|
command: ansible-galaxy role install --roles-path '{{ remote_tmp_dir }}/roles' safe-symlinks.tar
|
||||||
dest: '{{ remote_tmp_dir }}/dir-traversal/source/role_subdir'
|
args:
|
||||||
state: directory
|
chdir: "{{ remote_tmp_dir }}"
|
||||||
|
|
||||||
- copy:
|
- name: Validate each of the symlinks exists
|
||||||
dest: '{{ remote_tmp_dir }}/dir-traversal/source/role_subdir/.keep'
|
stat:
|
||||||
content: ''
|
path: "{{ remote_tmp_dir }}/roles/safe-symlinks.tar/{{ item }}"
|
||||||
|
loop:
|
||||||
|
- defaults/main.yml
|
||||||
|
- handlers/utils.yml
|
||||||
|
register: symlink_stat
|
||||||
|
|
||||||
- set_fact:
|
- assert:
|
||||||
installed_roles: "{{ remote_tmp_dir | realpath }}/dir-traversal/roles"
|
that:
|
||||||
|
- symlink_stat.results[0].stat.exists
|
||||||
|
- symlink_stat.results[0].stat.lnk_source == ((dest, 'roles/safe-symlinks.tar/defaults/common_vars/subdir/group0/main.yml') | path_join)
|
||||||
|
- symlink_stat.results[1].stat.exists
|
||||||
|
- symlink_stat.results[1].stat.lnk_source == ((dest, 'roles/safe-symlinks.tar/tasks/utils/suite.yml') | path_join)
|
||||||
|
vars:
|
||||||
|
dest: "{{ remote_tmp_dir | realpath }}"
|
||||||
|
|
||||||
- name: build role with symlink to a directory in the role
|
always:
|
||||||
script:
|
- name: Clean up
|
||||||
chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
|
file:
|
||||||
cmd: create-role-archive.py safe-link-dir.tar ./ role_subdir/..
|
path: "{{ item }}"
|
||||||
executable: '{{ ansible_playbook_python }}'
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
- name: install role successfully
|
loop:
|
||||||
command:
|
- "{{ remote_tmp_dir }}/roles/"
|
||||||
cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles safe-link-dir.tar'
|
- "{{ remote_tmp_dir }}/safe-symlinks.tar"
|
||||||
chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
|
|
||||||
register: galaxy_install_ok
|
|
||||||
|
|
||||||
- name: check for the directory symlink in the role
|
|
||||||
stat:
|
|
||||||
path: "{{ installed_roles }}/safe-link-dir.tar/symlink"
|
|
||||||
register: symlink_in_role
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- symlink_in_role.stat.exists
|
|
||||||
- symlink_in_role.stat.lnk_source == installed_roles + '/safe-link-dir.tar'
|
|
||||||
|
|
||||||
- name: remove tarfile for next test
|
|
||||||
file:
|
|
||||||
path: '{{ remote_tmp_dir }}/dir-traversal/source/safe-link-dir.tar'
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: build role with safe relative symlink
|
|
||||||
script:
|
|
||||||
chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
|
|
||||||
cmd: create-role-archive.py safe.tar ./ role_subdir/../context.txt
|
|
||||||
executable: '{{ ansible_playbook_python }}'
|
|
||||||
|
|
||||||
- name: install role successfully
|
|
||||||
command:
|
|
||||||
cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles safe.tar'
|
|
||||||
chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
|
|
||||||
register: galaxy_install_ok
|
|
||||||
|
|
||||||
- name: check for symlink in role
|
|
||||||
stat:
|
|
||||||
path: "{{ installed_roles }}/safe.tar/symlink"
|
|
||||||
register: symlink_in_role
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- symlink_in_role.stat.exists
|
|
||||||
- symlink_in_role.stat.lnk_source == installed_roles + '/safe.tar/context.txt'
|
|
||||||
|
|
||||||
- name: remove test directories
|
|
||||||
file:
|
|
||||||
path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}'
|
|
||||||
state: absent
|
|
||||||
loop:
|
|
||||||
- source
|
|
||||||
- target
|
|
||||||
- roles
|
|
||||||
|
|||||||
Reference in New Issue
Block a user