Ansible: malicious role archive can cause ansible-galaxy to overwrite arbitrary files
Description
An absolute path traversal attack exists in the Ansible automation platform. This flaw allows an attacker to craft a malicious Ansible role and make the victim execute the role. A symlink can be used to overwrite a file outside of the extraction path.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2023-5115 is an absolute path traversal in Ansible Automation Platform where a malicious role with a symlink can overwrite files outside the extraction directory.
Vulnerability
CVE-2023-5115 is an absolute path traversal vulnerability in the Red Hat Ansible Automation Platform. The flaw exists when a victim executes a malicious Ansible role: the role can contain a symlink that points to an arbitrary file outside the intended extraction path, allowing an attacker to overwrite files on the target system [1][2][4].
Exploitation
An attacker must first craft a malicious Ansible role containing a symlink that references an absolute path (e.g., /etc/passwd or another sensitive file). The victim must then apply this role—typically by downloading it from an untrusted source or being tricked into running it—which triggers the extraction process and follows the symlink to overwrite the targeted file [2][4]. No additional authentication beyond the victim's execution context is required; the impact is limited to the privileges of the user running Ansible.
Impact
Successful exploitation allows an attacker to overwrite arbitrary files on the victim's system, potentially leading to privilege escalation, denial of service, or code execution depending on the file targeted. Because the attacker controls the file content written through the symlink, the consequences can be severe [1][2].
Mitigation
Red Hat has released errata RHSA-2023:5701 and RHSA-2023:5758 to address this vulnerability in Ansible Automation Platform. Administrators should update to the patched versions immediately. As a workaround, users should only execute trusted Ansible roles and avoid extracting roles from untrusted sources [1][3].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansiblePyPI | < 8.5.0 | 8.5.0 |
Affected products
8- Red Hat/Red Hat Ansible Automation Platform 1.2v5cpe:/a:redhat:ansible_automation_platform
- Red Hat/Red Hat Ansible Automation Platform 2.4 for RHEL 9v5cpe:/a:redhat:ansible_automation_platform:2.4::el8Range: 0:2.15.5-1.el9ap
- Red Hat/Red Hat Ansible Automation Platform 2.3 for RHEL 9v5cpe:/a:redhat:ansible_automation_platform_developer:2.3::el9Range: 0:2.14.11-1.el9ap
- ghsa-coords5 versionspkg:pypi/ansiblepkg:rpm/opensuse/ansible-core-2.17&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/ansible-core-2.18&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/ansible-core-2.19&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/ansible-core&distro=openSUSE%20Tumbleweed
< 8.5.0+ 4 more
- (no CPE)range: < 8.5.0
- (no CPE)range: < 2.17.6-1.1
- (no CPE)range: < 2.18.10-2.1
- (no CPE)range: < 2.19.4-1.1
- (no CPE)range: < 2.16.9-1.1
Patches
11e930684bc0a[stable-2.15] Prevent roles from using symlinks to overwrite files outside of the installation directory (#81780) (#81785)
5 files changed · +123 −11
changelogs/fragments/cve-2023-5115.yml+3 −0 added@@ -0,0 +1,3 @@ +security_fixes: +- ansible-galaxy - Prevent roles from using symlinks to overwrite + files outside of the installation directory (CVE-2023-5115)
lib/ansible/galaxy/role.py+29 −11 modified@@ -394,18 +394,36 @@ def install(self): # bits that might be in the file for security purposes # and drop any containing directory, as mentioned above if member.isreg() or member.issym(): - n_member_name = to_native(member.name) - n_archive_parent_dir = to_native(archive_parent_dir) - n_parts = n_member_name.replace(n_archive_parent_dir, "", 1).split(os.sep) - n_final_parts = [] - for n_part in n_parts: - # TODO if the condition triggers it produces a broken installation. - # It will create the parent directory as an empty file and will - # explode if the directory contains valid files. - # Leaving this as is since the whole module needs a rewrite. - if n_part != '..' and not n_part.startswith('~') and '$' not in n_part: + for attr in ('name', 'linkname'): + attr_value = getattr(member, attr, None) + if not attr_value: + continue + n_attr_value = to_native(attr_value) + n_archive_parent_dir = to_native(archive_parent_dir) + n_parts = n_attr_value.replace(n_archive_parent_dir, "", 1).split(os.sep) + n_final_parts = [] + for n_part in n_parts: + # TODO if the condition triggers it produces a broken installation. + # It will create the parent directory as an empty file and will + # explode if the directory contains valid files. + # Leaving this as is since the whole module needs a rewrite. + # + # Check if we have any files with illegal names, + # and display a warning if so. This could help users + # to debug a broken installation. + if not n_part: + continue + if n_part == '..': + display.warning(f"Illegal filename '{n_part}': '..' is not allowed") + continue + if n_part.startswith('~'): + display.warning(f"Illegal filename '{n_part}': names cannot start with '~'") + continue + if '$' in n_part: + display.warning(f"Illegal filename '{n_part}': names cannot contain '$'") + continue n_final_parts.append(n_part) - member.name = os.path.join(*n_final_parts) + setattr(member, attr, os.path.join(*n_final_parts)) if _check_working_data_filter(): # deprecated: description='extract fallback without filter' python_version='3.11'
test/integration/targets/ansible-galaxy-role/files/create-role-archive.py+45 −0 added@@ -0,0 +1,45 @@ +#!/usr/bin/env python +"""Create a role archive which overwrites an arbitrary file.""" + +import argparse +import pathlib +import tarfile +import tempfile + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('archive', type=pathlib.Path, help='archive to create') + parser.add_argument('content', type=pathlib.Path, help='content to write') + parser.add_argument('target', type=pathlib.Path, help='file to overwrite') + + args = parser.parse_args() + + create_archive(args.archive, args.content, args.target) + + +def create_archive(archive_path: pathlib.Path, content_path: pathlib.Path, target_path: pathlib.Path) -> None: + with ( + tarfile.open(name=archive_path, mode='w') as role_archive, + tempfile.TemporaryDirectory() as temp_dir_name, + ): + temp_dir_path = pathlib.Path(temp_dir_name) + + meta_main_path = temp_dir_path / 'meta' / 'main.yml' + meta_main_path.parent.mkdir() + meta_main_path.write_text('') + + symlink_path = temp_dir_path / 'symlink' + symlink_path.symlink_to(target_path) + + role_archive.add(meta_main_path) + role_archive.add(symlink_path) + + content_tarinfo = role_archive.gettarinfo(content_path, str(symlink_path)) + + with content_path.open('rb') as content_file: + role_archive.addfile(content_tarinfo, content_file) + + +if __name__ == '__main__': + main()
test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml+44 −0 added@@ -0,0 +1,44 @@ +- name: create test directories + file: + path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}' + state: directory + loop: + - source + - target + - roles + +- name: create test content + copy: + dest: '{{ remote_tmp_dir }}/dir-traversal/source/content.txt' + content: | + some content to write + +- name: build dangerous dir traversal role + script: + chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + cmd: create-role-archive.py dangerous.tar content.txt {{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt + executable: '{{ ansible_playbook_python }}' + +- name: install dangerous role + command: + cmd: ansible-galaxy role install --roles-path '{{ remote_tmp_dir }}/dir-traversal/roles' dangerous.tar + chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + ignore_errors: true + register: galaxy_install_dangerous + +- name: check for overwritten file + stat: + path: '{{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt' + register: dangerous_overwrite_stat + +- name: get overwritten content + slurp: + path: '{{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt' + register: dangerous_overwrite_content + when: dangerous_overwrite_stat.stat.exists + +- assert: + that: + - dangerous_overwrite_content.content|default('')|b64decode == '' + - not dangerous_overwrite_stat.stat.exists + - galaxy_install_dangerous is failed
test/integration/targets/ansible-galaxy-role/tasks/main.yml+2 −0 modified@@ -59,3 +59,5 @@ - name: Uninstall invalid role command: ansible-galaxy role remove invalid-testrole + +- import_tasks: dir-traversal.yml
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- access.redhat.com/errata/RHSA-2023:5701ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2023:5758ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-jpvw-p8pr-9g2xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-5115ghsaADVISORY
- access.redhat.com/security/cve/CVE-2023-5115ghsavdb-entryx_refsource_REDHATWEB
- bugzilla.redhat.com/show_bug.cgighsaissue-trackingx_refsource_REDHATWEB
- github.com/ansible-community/ansible-build-data/blob/16d36538b96c65d9e0e28d89781361b69857ac0e/8/CHANGELOG-v8.rstghsaWEB
- github.com/ansible/ansible/commit/1e930684bc0a76ec3d094cd326738ad26416541cghsaWEB
- lists.debian.org/debian-lts-announce/2023/12/msg00018.htmlghsaWEB
News mentions
0No linked articles in our index yet.