VYPR
Moderate severityNVD Advisory· Published Mar 24, 2020· Updated Aug 4, 2024

CVE-2020-10684

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.

PackageAffected versionsPatched versions
ansiblePyPI
>= 2.7.0a1, < 2.7.172.7.17
ansiblePyPI
>= 2.8.0a1, < 2.8.112.8.11
ansiblePyPI
>= 2.9.0a1, < 2.9.72.9.7

Affected products

1

Patches

4
1d0d2645eed3

prevent ansible_facts injection (#68431) (#68446)

https://github.com/ansible/ansibleBrian CocaApr 15, 2020via ghsa
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():
    
5eabf7bb93c9

prevent ansible_facts injection (#68431) (#68445)

https://github.com/ansible/ansibleBrian CocaApr 15, 2020via ghsa
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',
         ])
    
0b4788a71fc7

prevent ansible_facts injection (#68431)

https://github.com/ansible/ansibleBrian CocaMar 24, 2020via ghsa
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
    
a9d2ceafe429

prevent ansible_facts injection (#68431)

https://github.com/ansible/ansibleBrian CocaMar 24, 2020via ghsa
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

News mentions

0

No linked articles in our index yet.