diff --git a/changelogs/fragments/fix-cloning-malformed-role-requirements.yml b/changelogs/fragments/fix-cloning-malformed-role-requirements.yml new file mode 100644 index 00000000000..8d0b179c70e --- /dev/null +++ b/changelogs/fragments/fix-cloning-malformed-role-requirements.yml @@ -0,0 +1,2 @@ +security_fixes: + - ansible-galaxy install - Ensure role requirements are passed as positional arguments to :command:`git clone`. Previously, a malicious role author could inject arbitrary git configuration in role dependencies. (CVE-2026-11332) diff --git a/lib/ansible/utils/galaxy.py b/lib/ansible/utils/galaxy.py index bbb26fb1115..b463ab0f084 100644 --- a/lib/ansible/utils/galaxy.py +++ b/lib/ansible/utils/galaxy.py @@ -74,7 +74,7 @@ def scm_archive_resource(src, scm='git', name=None, version='HEAD', keep_scm_met elif scm == 'hg': clone_cmd.append('--insecure') - clone_cmd.extend([src, name]) + clone_cmd.extend(['--', src, name]) run_scm_cmd(clone_cmd, tempdir) diff --git a/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml b/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml new file mode 100644 index 00000000000..2bb43fc1510 --- /dev/null +++ b/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml @@ -0,0 +1,52 @@ +- vars: + invalid_git_opts: '-ccore.sshCommand=sh -c "id > {{ remote_tmp_dir }}/role_exe"' + # use SSH protocol to test core.sshCommand is not configured + dummy_repo: git@github.com:ansible/nosuchrepo.git + block: + - name: Ensure git is installed + package: + name: git + when: ansible_distribution not in ["MacOSX", "Alpine"] + register: git_install + + - name: Create invalid requirements file + copy: + dest: "{{ remote_tmp_dir }}/invalid-requirements.yml" + content: | + - src: {{ invalid_git_opts }} + scm: git + name: {{ dummy_repo }} + - src: {{ dummy_repo }} + scm: git + name: {{ invalid_git_opts }} + + - name: Attempt to install invalid role requirements + command: ansible-galaxy install -r {{ remote_tmp_dir }}/invalid-requirements.yml --ignore-errors + register: result + environment: + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False + + - name: Validate git core.sshCommand did not run + stat: + path: "{{ remote_tmp_dir }}/role_exe" + register: stat_result + failed_when: stat_result.stat.exists + + - name: Verify the invalid field is treated as a single positional argument (repo or dest) + assert: + that: + - stderr is search(error1) + - stderr is search(error2) + - (stderr | regex_findall("git clone") | length) == (stderr | regex_findall("git clone --") | length) == 2 + vars: + stderr: "{{ result.stderr | regex_replace('\\n', ' ') }}" + error1: "repository '{{ invalid_git_opts }}' does not exist" + error2: "Cloning into '{{ invalid_git_opts }}'" + + always: + - name: Uninstall git if it was installed + package: + name: git + state: absent + when: git_install is changed | default(false) diff --git a/test/integration/targets/ansible-galaxy-role/tasks/main.yml b/test/integration/targets/ansible-galaxy-role/tasks/main.yml index 5f88a557652..f41273b4880 100644 --- a/test/integration/targets/ansible-galaxy-role/tasks/main.yml +++ b/test/integration/targets/ansible-galaxy-role/tasks/main.yml @@ -70,3 +70,4 @@ - import_tasks: dir-traversal.yml - import_tasks: valid-role-symlinks.yml +- import_tasks: git-config-injection.yml