VYPR
Moderate severityNVD Advisory· Published Dec 18, 2023· Updated Nov 20, 2025

Ansible: malicious role archive can cause ansible-galaxy to overwrite arbitrary files

CVE-2023-5115

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.

PackageAffected versionsPatched versions
ansiblePyPI
< 8.5.08.5.0

Affected products

8

Patches

1
1e930684bc0a

[stable-2.15] Prevent roles from using symlinks to overwrite files outside of the installation directory (#81780) (#81785)

https://github.com/ansible/ansibleMatt MartzSep 27, 2023via ghsa
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

News mentions

0

No linked articles in our index yet.