CVE-2020-10684
Description
A flaw was found in Ansible Engine, all versions 2.7.x, 2.8.x and 2.9.x prior to 2.7.17, 2.8.9 and 2.9.6 respectively, when using ansible_facts as a subkey of itself and promoting it to a variable when inject is enabled, overwriting the ansible_facts after the clean. An attacker could take advantage of this by altering the ansible_facts, such as ansible_hosts, users and any other key data which would lead into privilege escalation or code injection.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansiblePyPI | >= 2.7.0a1, < 2.7.17 | 2.7.17 |
ansiblePyPI | >= 2.8.0a1, < 2.8.11 | 2.8.11 |
ansiblePyPI | >= 2.9.0a1, < 2.9.7 | 2.9.7 |
Affected products
1Patches
41d0d2645eed3prevent ansible_facts injection (#68431) (#68446)
7 files changed · +36 −5
changelogs/fragments/af_clean.yml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - Ensure we don't allow ansible_facts subkey of ansible_facts to override top level, also fix 'deprefixing' to prevent key transforms.
lib/ansible/constants.py+1 −1 modified@@ -112,7 +112,7 @@ def set_constant(name, value, export=vars()): LOCALHOST = ('127.0.0.1', 'localhost', '::1') MODULE_REQUIRE_ARGS = ('command', 'win_command', 'shell', 'win_shell', 'raw', 'script') MODULE_NO_JSON = ('command', 'win_command', 'shell', 'win_shell', 'raw') -RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python') +RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python', 'ansible_facts') TREE_DIR = None VAULT_VERSION_MIN = 1.0 VAULT_VERSION_MAX = 1.0
lib/ansible/vars/clean.py+3 −4 modified@@ -156,10 +156,9 @@ def namespace_facts(facts): ''' return all facts inside 'ansible_facts' w/o an ansible_ prefix ''' deprefixed = {} for k in facts: - if k in ('ansible_local',): - # exceptions to 'deprefixing' - deprefixed[k] = module_response_deepcopy(facts[k]) + if k.startswith('ansible_') and k not in ('ansible_local',): + deprefixed[k[8:]] = module_response_deepcopy(facts[k]) else: - deprefixed[k.replace('ansible_', '', 1)] = module_response_deepcopy(facts[k]) + deprefixed[k] = module_response_deepcopy(facts[k]) return {'ansible_facts': deprefixed}
test/integration/targets/gathering_facts/library/bogus_facts+12 −0 added@@ -0,0 +1,12 @@ +#!/bin/sh + +echo '{ + "changed": false, + "ansible_facts": { + "ansible_facts": { + "discovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python", + "bogus_overwrite": "yes" + }, + "dansible_iscovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python" + } +}'
test/integration/targets/gathering_facts/runme.sh+3 −0 modified@@ -7,3 +7,6 @@ ansible-playbook test_gathering_facts.yml -i inventory -v "$@" # ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i inventory -v "$@" ANSIBLE_GATHERING=smart ansible-playbook test_run_once.yml -i inventory -v "$@" + +# ensure clean_facts is working properly +ansible-playbook test_prevent_injection.yml -i inventory -v "$@"
test/integration/targets/gathering_facts/test_prevent_injection.yml+14 −0 added@@ -0,0 +1,14 @@ +- name: Ensure clean_facts is working properly + hosts: facthost1 + gather_facts: false + tasks: + - name: gather 'bad' facts + action: bogus_facts + + - name: ensure that the 'bad' facts didn't polute what they are not supposed to + assert: + that: + - "'touch' not in discovered_interpreter_python|default('')" + - "'touch' not in ansible_facts.get('discovered_interpreter_python', '')" + - "'touch' not in ansible_facts.get('ansible_facts', {}).get('discovered_interpreter_python', '')" + - bogus_overwrite is undefined
test/sanity/code-smell/shebang.py+1 −0 modified@@ -28,6 +28,7 @@ def main(): 'test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1', 'test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1', 'test/utils/shippable/timing.py', + 'test/integration/targets/gathering_facts/library/bogus_facts', ]) for path in sys.argv[1:] or sys.stdin.read().splitlines():
5eabf7bb93c9prevent ansible_facts injection (#68431) (#68445)
7 files changed · +36 −6
changelogs/fragments/af_clean.yml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - Ensure we don't allow ansible_facts subkey of ansible_facts to override top level, also fix 'deprefixing' to prevent key transforms.
lib/ansible/constants.py+1 −1 modified@@ -113,7 +113,7 @@ def __getitem__(self, y): LOCALHOST = ('127.0.0.1', 'localhost', '::1') MODULE_REQUIRE_ARGS = ('command', 'win_command', 'shell', 'win_shell', 'raw', 'script') MODULE_NO_JSON = ('command', 'win_command', 'shell', 'win_shell', 'raw') -RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python') +RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python', 'ansible_facts') TREE_DIR = None VAULT_VERSION_MIN = 1.0 VAULT_VERSION_MAX = 1.0
lib/ansible/vars/clean.py+3 −5 modified@@ -16,7 +16,6 @@ from ansible.plugins.loader import connection_loader from ansible.utils.display import Display - display = Display() @@ -172,10 +171,9 @@ def namespace_facts(facts): ''' return all facts inside 'ansible_facts' w/o an ansible_ prefix ''' deprefixed = {} for k in facts: - if k in ('ansible_local',): - # exceptions to 'deprefixing' - deprefixed[k] = module_response_deepcopy(facts[k]) + if k.startswith('ansible_') and k not in ('ansible_local',): + deprefixed[k[8:]] = module_response_deepcopy(facts[k]) else: - deprefixed[k.replace('ansible_', '', 1)] = module_response_deepcopy(facts[k]) + deprefixed[k] = module_response_deepcopy(facts[k]) return {'ansible_facts': deprefixed}
test/integration/targets/gathering_facts/library/bogus_facts+12 −0 added@@ -0,0 +1,12 @@ +#!/bin/sh + +echo '{ + "changed": false, + "ansible_facts": { + "ansible_facts": { + "discovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python", + "bogus_overwrite": "yes" + }, + "dansible_iscovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python" + } +}'
test/integration/targets/gathering_facts/runme.sh+3 −0 modified@@ -7,3 +7,6 @@ ansible-playbook test_gathering_facts.yml -i inventory -v "$@" # ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i inventory -v "$@" ANSIBLE_GATHERING=smart ansible-playbook test_run_once.yml -i inventory -v "$@" + +# ensure clean_facts is working properly +ansible-playbook test_prevent_injection.yml -i inventory -v "$@"
test/integration/targets/gathering_facts/test_prevent_injection.yml+14 −0 added@@ -0,0 +1,14 @@ +- name: Ensure clean_facts is working properly + hosts: facthost1 + gather_facts: false + tasks: + - name: gather 'bad' facts + action: bogus_facts + + - name: ensure that the 'bad' facts didn't polute what they are not supposed to + assert: + that: + - "'touch' not in discovered_interpreter_python|default('')" + - "'touch' not in ansible_facts.get('discovered_interpreter_python', '')" + - "'touch' not in ansible_facts.get('ansible_facts', {}).get('discovered_interpreter_python', '')" + - bogus_overwrite is undefined
test/sanity/code-smell/shebang.py+1 −0 modified@@ -38,6 +38,7 @@ def main(): 'test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1', 'test/utils/shippable/timing.py', 'test/integration/targets/old_style_modules_posix/library/helloworld.sh', + 'test/integration/targets/gathering_facts/library/bogus_facts', # Python 3-only. Only run by release engineers 'hacking/release-announcement.py', ])
0b4788a71fc7prevent ansible_facts injection (#68431)
7 files changed · +36 −6
changelogs/fragments/af_clean.yml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - Ensure we don't allow ansible_facts subkey of ansible_facts to override top level, also fix 'deprefixing' to prevent key transforms.
lib/ansible/constants.py+1 −1 modified@@ -110,7 +110,7 @@ def __getitem__(self, y): LOCALHOST = ('127.0.0.1', 'localhost', '::1') MODULE_REQUIRE_ARGS = ('command', 'win_command', 'shell', 'win_shell', 'raw', 'script') MODULE_NO_JSON = ('command', 'win_command', 'shell', 'win_shell', 'raw') -RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python') +RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python', 'ansible_facts') TREE_DIR = None VAULT_VERSION_MIN = 1.0 VAULT_VERSION_MAX = 1.0
lib/ansible/vars/clean.py+3 −5 modified@@ -16,7 +16,6 @@ from ansible.plugins.loader import connection_loader from ansible.utils.display import Display - display = Display() @@ -172,10 +171,9 @@ def namespace_facts(facts): ''' return all facts inside 'ansible_facts' w/o an ansible_ prefix ''' deprefixed = {} for k in facts: - if k in ('ansible_local',): - # exceptions to 'deprefixing' - deprefixed[k] = module_response_deepcopy(facts[k]) + if k.startswith('ansible_') and k not in ('ansible_local',): + deprefixed[k[8:]] = module_response_deepcopy(facts[k]) else: - deprefixed[k.replace('ansible_', '', 1)] = module_response_deepcopy(facts[k]) + deprefixed[k] = module_response_deepcopy(facts[k]) return {'ansible_facts': deprefixed}
test/integration/targets/gathering_facts/library/bogus_facts+12 −0 added@@ -0,0 +1,12 @@ +#!/bin/sh + +echo '{ + "changed": false, + "ansible_facts": { + "ansible_facts": { + "discovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python", + "bogus_overwrite": "yes" + }, + "dansible_iscovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python" + } +}'
test/integration/targets/gathering_facts/runme.sh+3 −0 modified@@ -7,3 +7,6 @@ ansible-playbook test_gathering_facts.yml -i inventory -v "$@" # ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i inventory -v "$@" ANSIBLE_GATHERING=smart ansible-playbook test_run_once.yml -i inventory -v "$@" + +# ensure clean_facts is working properly +ansible-playbook test_prevent_injection.yml -i inventory -v "$@"
test/integration/targets/gathering_facts/test_prevent_injection.yml+14 −0 added@@ -0,0 +1,14 @@ +- name: Ensure clean_facts is working properly + hosts: facthost1 + gather_facts: false + tasks: + - name: gather 'bad' facts + action: bogus_facts + + - name: ensure that the 'bad' facts didn't polute what they are not supposed to + assert: + that: + - "'touch' not in discovered_interpreter_python|default('')" + - "'touch' not in ansible_facts.get('discovered_interpreter_python', '')" + - "'touch' not in ansible_facts.get('ansible_facts', {}).get('discovered_interpreter_python', '')" + - bogus_overwrite is undefined
test/sanity/ignore.txt+1 −0 modified@@ -5951,6 +5951,7 @@ test/integration/targets/collections_relative_imports/collection_root/ansible_co test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py pylint:relative-beyond-top-level test/integration/targets/expect/files/test_command.py future-import-boilerplate test/integration/targets/expect/files/test_command.py metaclass-boilerplate +test/integration/targets/gathering_facts/library/bogus_facts shebang test/integration/targets/get_url/files/testserver.py future-import-boilerplate test/integration/targets/get_url/files/testserver.py metaclass-boilerplate test/integration/targets/group/files/gidget.py future-import-boilerplate
a9d2ceafe429prevent ansible_facts injection (#68431)
7 files changed · +36 −6
changelogs/fragments/af_clean.yml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - Ensure we don't allow ansible_facts subkey of ansible_facts to override top level, also fix 'deprefixing' to prevent key transforms.
lib/ansible/constants.py+1 −1 modified@@ -112,7 +112,7 @@ def __getitem__(self, y): 'ansible.windows.win_shell', 'raw', 'script') MODULE_NO_JSON = ('command', 'win_command', 'ansible.windows.win_command', 'shell', 'win_shell', 'ansible.windows.win_shell', 'raw') -RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python') +RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python', 'ansible_facts') TREE_DIR = None VAULT_VERSION_MIN = 1.0 VAULT_VERSION_MAX = 1.0
lib/ansible/vars/clean.py+3 −5 modified@@ -16,7 +16,6 @@ from ansible.plugins.loader import connection_loader from ansible.utils.display import Display - display = Display() @@ -169,10 +168,9 @@ def namespace_facts(facts): ''' return all facts inside 'ansible_facts' w/o an ansible_ prefix ''' deprefixed = {} for k in facts: - if k in ('ansible_local',): - # exceptions to 'deprefixing' - deprefixed[k] = module_response_deepcopy(facts[k]) + if k.startswith('ansible_') and k not in ('ansible_local',): + deprefixed[k[8:]] = module_response_deepcopy(facts[k]) else: - deprefixed[k.replace('ansible_', '', 1)] = module_response_deepcopy(facts[k]) + deprefixed[k] = module_response_deepcopy(facts[k]) return {'ansible_facts': deprefixed}
test/integration/targets/gathering_facts/library/bogus_facts+12 −0 added@@ -0,0 +1,12 @@ +#!/bin/sh + +echo '{ + "changed": false, + "ansible_facts": { + "ansible_facts": { + "discovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python", + "bogus_overwrite": "yes" + }, + "dansible_iscovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python" + } +}'
test/integration/targets/gathering_facts/runme.sh+3 −0 modified@@ -7,3 +7,6 @@ ansible-playbook test_gathering_facts.yml -i inventory -v "$@" # ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i inventory -v "$@" ANSIBLE_GATHERING=smart ansible-playbook test_run_once.yml -i inventory -v "$@" + +# ensure clean_facts is working properly +ansible-playbook test_prevent_injection.yml -i inventory -v "$@"
test/integration/targets/gathering_facts/test_prevent_injection.yml+14 −0 added@@ -0,0 +1,14 @@ +- name: Ensure clean_facts is working properly + hosts: facthost1 + gather_facts: false + tasks: + - name: gather 'bad' facts + action: bogus_facts + + - name: ensure that the 'bad' facts didn't polute what they are not supposed to + assert: + that: + - "'touch' not in discovered_interpreter_python|default('')" + - "'touch' not in ansible_facts.get('discovered_interpreter_python', '')" + - "'touch' not in ansible_facts.get('ansible_facts', {}).get('discovered_interpreter_python', '')" + - bogus_overwrite is undefined
test/sanity/ignore.txt+1 −0 modified@@ -269,6 +269,7 @@ test/integration/targets/collections_relative_imports/collection_root/ansible_co test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py pylint:relative-beyond-top-level test/integration/targets/expect/files/test_command.py future-import-boilerplate test/integration/targets/expect/files/test_command.py metaclass-boilerplate +test/integration/targets/gathering_facts/library/bogus_facts shebang test/integration/targets/get_url/files/testserver.py future-import-boilerplate test/integration/targets/get_url/files/testserver.py metaclass-boilerplate test/integration/targets/group/files/gidget.py future-import-boilerplate
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
16- github.com/advisories/GHSA-p62g-jhg6-v3rqghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/DKPA4KC3OJSUFASUYMG66HKJE7ADNGFW/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/MRRYUU5ZBLPBXCYG6CFP35D64NP2UB2S/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/WQVOQD4VAIXXTVQAJKTN7NUGTJFE2PCB/mitrevendor-advisory
- nvd.nist.gov/vuln/detail/CVE-2020-10684ghsaADVISORY
- security.gentoo.org/glsa/202006-11ghsavendor-advisoryWEB
- www.debian.org/security/2021/dsa-4950ghsavendor-advisoryWEB
- bugzilla.redhat.com/show_bug.cgighsaWEB
- github.com/ansible/ansible/commit/0b4788a71fc7d24ffa957a94ee5e23d6a9733ab0ghsaWEB
- github.com/ansible/ansible/commit/1d0d2645eed36ac4e17052ab4eacf240132d96fbghsaWEB
- github.com/ansible/ansible/commit/5eabf7bb93c9bfc375b806a2b1f623d650cddc2bghsaWEB
- github.com/ansible/ansible/commit/a9d2ceafe429171c0e2ad007058b88bae57c74ceghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/ansible/PYSEC-2020-207.yamlghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DKPA4KC3OJSUFASUYMG66HKJE7ADNGFWghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MRRYUU5ZBLPBXCYG6CFP35D64NP2UB2SghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WQVOQD4VAIXXTVQAJKTN7NUGTJFE2PCBghsaWEB
News mentions
0No linked articles in our index yet.