diff --git a/CVE-2024-8775.patch b/CVE-2024-8775.patch new file mode 100644 index 0000000000000000000000000000000000000000..749efec79f7d694285505cdc6d7741be7d634e88 --- /dev/null +++ b/CVE-2024-8775.patch @@ -0,0 +1,488 @@ +From: Matt Davis <6775756+nitzmahone@users.noreply.github.com> +Date: Thu, 24 Oct 2024 15:56:54 -0700 +Subject: CVE-2024-8775 Preserve `_ansible_no_log` from action result; + fix `include_vars` to set properly (#84143) + +* fixes for CVE-2024-8775 + +* propagate truthy `_ansible_no_log` in action result (previously superseded by task-calculated value) +* always mask entire `include_vars` action result if any file loaded had a false `show_content` flag (previously used only the flag value from the last file loaded) + +* update no_log tests for CVE-2024-8775 +* include validation of _ansible_no_log preservation when set by actions +* replace static values with dynamic for increased robustness to logging/display/callback changes (but still using grep counts :( ) + +* changelog + +* use ternary, coerce to bool explicitly + +[backport] + "grep -q" exits on the first match, so the preceding +process in the pipeline gets SIGPIPE. Changing "grep -q +'porter.*cable'" to "grep 'porter.*cable' >/dev/null" makes that test +pass. + +origin: backport, https://github.com/ansible/ansible/commit/c9ac477e53a99e95781f333eec3329a935c1bf95 +bug: https://github.com/ansible/ansible/issues/84217 +bug-debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1086551 +--- + changelogs/fragments/cve-2024-8775.yml | 5 ++++ + lib/ansible/executor/task_executor.py | 3 +- + lib/ansible/plugins/action/include_vars.py | 3 +- + .../targets/include_vars-ad-hoc/dir/encrypted.yml | 6 ++++ + .../targets/include_vars-ad-hoc/runme.sh | 22 +++++++++++++-- + .../targets/include_vars-ad-hoc/vaultpass | 3 ++ + .../no_log/action_plugins/action_sets_no_log.py | 8 ++++++ + .../targets/no_log/ansible_no_log_in_result.yml | 13 +++++++++ + test/integration/targets/no_log/dynamic.yml | 29 +++++++++++++++----- + test/integration/targets/no_log/no_log_config.yml | 2 +- + test/integration/targets/no_log/no_log_local.yml | 15 ++++++---- + .../targets/no_log/no_log_suboptions.yml | 14 +++++----- + .../targets/no_log/no_log_suboptions_invalid.yml | 29 +++++++++++--------- + test/integration/targets/no_log/runme.sh | 16 +++++++---- + test/integration/targets/no_log/runme.sh.orig | 26 ++++++++++++++++++ + test/integration/targets/no_log/secretvars.yml | 32 ++++++++++++++++++++++ + 16 files changed, 182 insertions(+), 44 deletions(-) + create mode 100644 changelogs/fragments/cve-2024-8775.yml + create mode 100644 test/integration/targets/include_vars-ad-hoc/dir/encrypted.yml + create mode 100755 test/integration/targets/include_vars-ad-hoc/vaultpass + create mode 100644 test/integration/targets/no_log/action_plugins/action_sets_no_log.py + create mode 100644 test/integration/targets/no_log/ansible_no_log_in_result.yml + create mode 100755 test/integration/targets/no_log/runme.sh.orig + create mode 100644 test/integration/targets/no_log/secretvars.yml + +diff --git a/changelogs/fragments/cve-2024-8775.yml b/changelogs/fragments/cve-2024-8775.yml +new file mode 100644 +index 0000000..a292c99 +--- /dev/null ++++ b/changelogs/fragments/cve-2024-8775.yml +@@ -0,0 +1,5 @@ ++security_fixes: ++ - task result processing - Ensure that action-sourced result masking (``_ansible_no_log=True``) ++ is preserved. (CVE-2024-8775) ++ - include_vars action - Ensure that result masking is correctly requested when vault-encrypted ++ files are read. (CVE-2024-8775) +diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py +index 65bcacf..957d1bf 100644 +--- a/lib/ansible/executor/task_executor.py ++++ b/lib/ansible/executor/task_executor.py +@@ -677,8 +677,9 @@ class TaskExecutor: + self._handler.cleanup() + display.debug("handler run complete") + ++ # propagate no log to result- the action can set this, so only overwrite it with the task's value if missing or falsey + # preserve no log +- result["_ansible_no_log"] = self._play_context.no_log ++ result["_ansible_no_log"] = bool(self._play_context.no_log or result.get('_ansible_no_log', False)) + + # update the local copy of vars with the registered value, if specified, + # or any facts which may have been generated by the module execution +diff --git a/lib/ansible/plugins/action/include_vars.py b/lib/ansible/plugins/action/include_vars.py +index 0723453..bd67d01 100644 +--- a/lib/ansible/plugins/action/include_vars.py ++++ b/lib/ansible/plugins/action/include_vars.py +@@ -225,7 +225,8 @@ class ActionModule(ActionBase): + b_data, show_content = self._loader._get_file_contents(filename) + data = to_text(b_data, errors='surrogate_or_strict') + +- self.show_content = show_content ++ self.show_content &= show_content # mask all results if any file was encrypted ++ + data = self._loader.load(data, file_name=filename, show_content=show_content) + if not data: + data = dict() +diff --git a/test/integration/targets/include_vars-ad-hoc/dir/encrypted.yml b/test/integration/targets/include_vars-ad-hoc/dir/encrypted.yml +new file mode 100644 +index 0000000..328f180 +--- /dev/null ++++ b/test/integration/targets/include_vars-ad-hoc/dir/encrypted.yml +@@ -0,0 +1,6 @@ ++$ANSIBLE_VAULT;1.1;AES256 ++31613539636636336264396235633933633839646337323533316638633336653461393036336664 ++3939386435313638366366626566346135623932653238360a366261303663343034633865626132 ++31646231623630333636383636383833656331643164656366623332396439306132663264663131 ++6439633766376261320a616265306430366530363866356433366430633265353739373732646536 ++37623661333064306162373463616231636365373231313939373230643936313362 +diff --git a/test/integration/targets/include_vars-ad-hoc/runme.sh b/test/integration/targets/include_vars-ad-hoc/runme.sh +index 51b68d2..41a0929 100755 +--- a/test/integration/targets/include_vars-ad-hoc/runme.sh ++++ b/test/integration/targets/include_vars-ad-hoc/runme.sh +@@ -1,6 +1,22 @@ + #!/usr/bin/env bash + +-set -eux ++set -eux -o pipefail + +-ansible testhost -i ../../inventory -m include_vars -a 'dir/inc.yml' "$@" +-ansible testhost -i ../../inventory -m include_vars -a 'dir=dir' "$@" ++echo "single file include" ++ansible testhost -i ../../inventory -m include_vars -a 'dir/inc.yml' -vvv 2>&1 | grep 'porter.*cable' 2>&1 ++ ++echo "single file encrypted include" ++ansible testhost -i ../../inventory -m include_vars -a 'dir/encrypted.yml' -vvv --vault-password-file vaultpass > output.txt 2>&1 ++ ++echo "directory include with encrypted" ++ansible testhost -i ../../inventory -m include_vars -a 'dir=dir' -vvv --vault-password-file vaultpass >> output.txt 2>&1 ++ ++grep -q 'output has been hidden' output.txt ++ ++# all content should be masked if any file is encrypted ++if grep -e 'i am a secret' -e 'porter.*cable' output.txt; then ++ echo "FAIL: vault masking failed" ++ exit 1 ++fi ++ ++echo PASS +diff --git a/test/integration/targets/include_vars-ad-hoc/vaultpass b/test/integration/targets/include_vars-ad-hoc/vaultpass +new file mode 100755 +index 0000000..1f78d41 +--- /dev/null ++++ b/test/integration/targets/include_vars-ad-hoc/vaultpass +@@ -0,0 +1,3 @@ ++#!/bin/sh ++ ++echo supersecurepassword +diff --git a/test/integration/targets/no_log/action_plugins/action_sets_no_log.py b/test/integration/targets/no_log/action_plugins/action_sets_no_log.py +new file mode 100644 +index 0000000..cb42616 +--- /dev/null ++++ b/test/integration/targets/no_log/action_plugins/action_sets_no_log.py +@@ -0,0 +1,8 @@ ++from __future__ import annotations ++ ++from ansible.plugins.action import ActionBase ++ ++ ++class ActionModule(ActionBase): ++ def run(self, tmp=None, task_vars=None): ++ return dict(changed=False, failed=False, msg="action result should be masked", _ansible_no_log="yeppers") # ensure that a truthy non-bool works here +diff --git a/test/integration/targets/no_log/ansible_no_log_in_result.yml b/test/integration/targets/no_log/ansible_no_log_in_result.yml +new file mode 100644 +index 0000000..a80a4a7 +--- /dev/null ++++ b/test/integration/targets/no_log/ansible_no_log_in_result.yml +@@ -0,0 +1,13 @@ ++- hosts: localhost ++ gather_facts: no ++ tasks: ++ - action_sets_no_log: ++ register: res_action ++ ++ - assert: ++ that: ++ - res_action.msg == "action result should be masked" ++ ++ - action_sets_no_log: ++ loop: [1, 2, 3] ++ register: res_action +diff --git a/test/integration/targets/no_log/dynamic.yml b/test/integration/targets/no_log/dynamic.yml +index 4a1123d..9523677 100644 +--- a/test/integration/targets/no_log/dynamic.yml ++++ b/test/integration/targets/no_log/dynamic.yml +@@ -1,27 +1,42 @@ + - name: test dynamic no log + hosts: testhost + gather_facts: no +- ignore_errors: yes + tasks: + - name: no loop, task fails, dynamic no_log +- debug: +- msg: "SHOW {{ var_does_not_exist }}" ++ raw: echo {{ var_does_not_exist }} + no_log: "{{ not (unsafe_show_logs|bool) }}" ++ ignore_errors: yes ++ register: result ++ ++ - assert: ++ that: ++ - result is failed ++ - result.results is not defined + + - name: loop, task succeeds, dynamic does no_log +- debug: +- msg: "SHOW {{ item }}" ++ raw: echo {{ item }} + loop: + - a + - b + - c + no_log: "{{ not (unsafe_show_logs|bool) }}" ++ register: result ++ ++ - assert: ++ that: ++ - result.results | length == 3 + + - name: loop, task fails, dynamic no_log +- debug: +- msg: "SHOW {{ var_does_not_exist }}" ++ raw: echo {{ var_does_not_exist }} + loop: + - a + - b + - c + no_log: "{{ not (unsafe_show_logs|bool) }}" ++ ignore_errors: yes ++ register: result ++ ++ - assert: ++ that: ++ - result is failed ++ - result.results is not defined # DT needs result.results | length == 3 +diff --git a/test/integration/targets/no_log/no_log_config.yml b/test/integration/targets/no_log/no_log_config.yml +index 8a50880..165f4e0 100644 +--- a/test/integration/targets/no_log/no_log_config.yml ++++ b/test/integration/targets/no_log/no_log_config.yml +@@ -10,4 +10,4 @@ + - debug: + + - debug: +- loop: '{{ range(3) }}' ++ loop: '{{ range(3) | list }}' +diff --git a/test/integration/targets/no_log/no_log_local.yml b/test/integration/targets/no_log/no_log_local.yml +index aacf7de..d2bc5ae 100644 +--- a/test/integration/targets/no_log/no_log_local.yml ++++ b/test/integration/targets/no_log/no_log_local.yml +@@ -4,19 +4,22 @@ + hosts: testhost + gather_facts: no + tasks: ++ - include_vars: secretvars.yml ++ no_log: true ++ + - name: args should be logged in the absence of no_log +- shell: echo "LOG_ME_TASK_SUCCEEDED" ++ shell: echo "{{log_me_prefix}}TASK_SUCCEEDED" + + - name: failed args should be logged in the absence of no_log +- shell: echo "LOG_ME_TASK_FAILED" ++ shell: echo "{{log_me_prefix}}TASK_FAILED" + failed_when: true + ignore_errors: true + + - name: item args should be logged in the absence of no_log + shell: echo {{ item }} +- with_items: [ "LOG_ME_ITEM", "LOG_ME_SKIPPED", "LOG_ME_ITEM_FAILED" ] +- when: item != "LOG_ME_SKIPPED" +- failed_when: item == "LOG_ME_ITEM_FAILED" ++ with_items: [ "{{log_me_prefix}}ITEM", "{{log_me_prefix}}SKIPPED", "{{log_me_prefix}}ITEM_FAILED" ] ++ when: item != log_me_prefix ~ "SKIPPED" ++ failed_when: item == log_me_prefix ~ "ITEM_FAILED" + ignore_errors: true + + - name: args should not be logged when task-level no_log set +@@ -61,7 +64,7 @@ + no_log: true + + - name: args should be logged when task-level no_log overrides play-level +- shell: echo "LOG_ME_OVERRIDE" ++ shell: echo "{{log_me_prefix}}OVERRIDE" + no_log: false + + - name: Add a fake host for next play +diff --git a/test/integration/targets/no_log/no_log_suboptions.yml b/test/integration/targets/no_log/no_log_suboptions.yml +index e67ecfe..338a871 100644 +--- a/test/integration/targets/no_log/no_log_suboptions.yml ++++ b/test/integration/targets/no_log/no_log_suboptions.yml +@@ -5,20 +5,20 @@ + tasks: + - name: Task with suboptions + module: +- secret: GLAMOROUS ++ secret: "{{ s106 }}" + subopt_dict: +- str_sub_opt1: AFTERMATH ++ str_sub_opt1: "{{ s107 }}" + str_sub_opt2: otherstring + nested_subopt: +- n_subopt1: MANPOWER ++ n_subopt1: "{{ s101 }}" + + subopt_list: +- - subopt1: UNTAPPED ++ - subopt1: "{{ s102 }}" + subopt2: thridstring + +- - subopt1: CONCERNED ++ - subopt1: "{{ s103 }}" + + - name: Task with suboptions as string + module: +- secret: MARLIN +- subopt_dict: str_sub_opt1=FLICK ++ secret: "{{ s104 }}" ++ subopt_dict: str_sub_opt1={{ s105 }} +diff --git a/test/integration/targets/no_log/no_log_suboptions_invalid.yml b/test/integration/targets/no_log/no_log_suboptions_invalid.yml +index 933a8a9..1092cf5 100644 +--- a/test/integration/targets/no_log/no_log_suboptions_invalid.yml ++++ b/test/integration/targets/no_log/no_log_suboptions_invalid.yml +@@ -4,42 +4,45 @@ + ignore_errors: yes + + tasks: ++ - include_vars: secretvars.yml ++ no_log: true ++ + - name: Task with suboptions and invalid parameter + module: +- secret: SUPREME ++ secret: "{{ s201 }}" + invalid: param + subopt_dict: +- str_sub_opt1: IDIOM ++ str_sub_opt1: "{{ s202 }}" + str_sub_opt2: otherstring + nested_subopt: +- n_subopt1: MOCKUP ++ n_subopt1: "{{ s203 }}" + + subopt_list: +- - subopt1: EDUCATED ++ - subopt1: "{{ s204 }}" + subopt2: thridstring +- - subopt1: FOOTREST ++ - subopt1: "{{ s205 }}" + + - name: Task with suboptions as string with invalid parameter + module: +- secret: FOOTREST ++ secret: "{{ s213 }}" + invalid: param +- subopt_dict: str_sub_opt1=CRAFTY ++ subopt_dict: str_sub_opt1={{ s206 }} + + - name: Task with suboptions with dict instead of list + module: +- secret: FELINE ++ secret: "{{ s207 }}" + subopt_dict: +- str_sub_opt1: CRYSTAL ++ str_sub_opt1: "{{ s208 }}" + str_sub_opt2: otherstring + nested_subopt: +- n_subopt1: EXPECTANT ++ n_subopt1: "{{ s209 }}" + subopt_list: + foo: bar + + - name: Task with suboptions with incorrect data type + module: +- secret: AGROUND ++ secret: "{{ s210 }}" + subopt_dict: 9068.21361 + subopt_list: +- - subopt1: GOLIATH +- - subopt1: FREEFALL ++ - subopt1: "{{ s211 }}" ++ - subopt1: "{{ s212 }}" +diff --git a/test/integration/targets/no_log/runme.sh b/test/integration/targets/no_log/runme.sh +index 8bfe019..d6476ac 100755 +--- a/test/integration/targets/no_log/runme.sh ++++ b/test/integration/targets/no_log/runme.sh +@@ -1,6 +1,12 @@ + #!/usr/bin/env bash + +-set -eux ++set -eux -o pipefail ++ ++# ensure _ansible_no_log returned by actions is actually respected ++ansible-playbook ansible_no_log_in_result.yml -vvvvv > "${OUTPUT_DIR}/output.log" 2> /dev/null ++ ++[ "$(grep -c "action result should be masked" "${OUTPUT_DIR}/output.log")" = "0" ] ++[ "$(grep -c "the output has been hidden" "${OUTPUT_DIR}/output.log")" = "4" ] + + # This test expects 7 loggable vars and 0 non-loggable ones. + # If either mismatches it fails, run the ansible-playbook command to debug. +@@ -9,18 +15,18 @@ set -eux + + # deal with corner cases with no log and loops + # no log enabled, should produce 6 censored messages +-[ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=no|grep -c 'output has been hidden')" = "6" ] ++[ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=no|grep -c 'output has been hidden')" = "6" ] # DT needs 7 + + # no log disabled, should produce 0 censored + [ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ] + + # test no log for sub options +-[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ] ++[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'SECRET')" = "0" ] + + # test invalid data passed to a suboption +-[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ] ++[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'SECRET')" = "0" ] + + # test variations on ANSIBLE_NO_LOG + [ "$(ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] + [ "$(ANSIBLE_NO_LOG=0 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] +-[ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "6" ] ++[ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "6" ] # DT needs 5 +diff --git a/test/integration/targets/no_log/runme.sh.orig b/test/integration/targets/no_log/runme.sh.orig +new file mode 100755 +index 0000000..8bfe019 +--- /dev/null ++++ b/test/integration/targets/no_log/runme.sh.orig +@@ -0,0 +1,26 @@ ++#!/usr/bin/env bash ++ ++set -eux ++ ++# This test expects 7 loggable vars and 0 non-loggable ones. ++# If either mismatches it fails, run the ansible-playbook command to debug. ++[ "$(ansible-playbook no_log_local.yml -i ../../inventory -vvvvv "$@" | awk \ ++'BEGIN { logme = 0; nolog = 0; } /LOG_ME/ { logme += 1;} /DO_NOT_LOG/ { nolog += 1;} END { printf "%d/%d", logme, nolog; }')" = "26/0" ] ++ ++# deal with corner cases with no log and loops ++# no log enabled, should produce 6 censored messages ++[ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=no|grep -c 'output has been hidden')" = "6" ] ++ ++# no log disabled, should produce 0 censored ++[ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ] ++ ++# test no log for sub options ++[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ] ++ ++# test invalid data passed to a suboption ++[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ] ++ ++# test variations on ANSIBLE_NO_LOG ++[ "$(ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] ++[ "$(ANSIBLE_NO_LOG=0 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] ++[ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "6" ] +diff --git a/test/integration/targets/no_log/secretvars.yml b/test/integration/targets/no_log/secretvars.yml +new file mode 100644 +index 0000000..0030d74 +--- /dev/null ++++ b/test/integration/targets/no_log/secretvars.yml +@@ -0,0 +1,32 @@ ++# These values are in a separate vars file and referenced dynamically to avoid spurious counts from contextual error messages ++# that show the playbook contents inline (since unencrypted playbook contents are not considered secret). ++log_me_prefix: LOG_ME_ ++ ++# Unique values are used for each secret below to ensure that one secret "learned" does not cause another non-secret ++# value to be considered secret simply because they share the same value. A common substring is, however, present in ++# each one to simplify searching for secret values in test output. Having a unique value for each also helps in ++# debugging when unexpected output is encountered. ++ ++# secrets for no_log_suboptions.yml ++s101: SECRET101 ++s102: SECRET102 ++s103: SECRET103 ++s104: SECRET104 ++s105: SECRET105 ++s106: SECRET106 ++s107: SECRET107 ++ ++# secrets for no_log_suboptions_invalid.yml ++s201: SECRET201 ++s202: SECRET202 ++s203: SECRET203 ++s204: SECRET204 ++s205: SECRET205 ++s206: SECRET206 ++s207: SECRET207 ++s208: SECRET208 ++s209: SECRET209 ++s210: SECRET210 ++s211: SECRET211 ++s212: SECRET212 ++s213: SECRET213 diff --git a/CVE-2024-9902.patch b/CVE-2024-9902.patch new file mode 100644 index 0000000000000000000000000000000000000000..ef9104f4bd56ecc60f064cfb9617d8702ec00188 --- /dev/null +++ b/CVE-2024-9902.patch @@ -0,0 +1,146 @@ +From: Brian Coca +Date: Mon, 28 Oct 2024 12:47:08 -0400 +Subject: CVE-2024-9902 user module avoid chmoding symlink'd home file + (#83956) (#84081) + +also added tests + +origin: https://github.com/ansible/ansible/commit/3b5a4319985e1eabe7fc0410bf8308b671f4f586 +bug: https://github.com/ansible/ansible/pull/84081 +bug-debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1086883 +--- + changelogs/fragments/user_action_fix.yml | 2 + + lib/ansible/modules/system/user.py | 4 +- + .../targets/user/files/skel/.ssh/known_hosts | 1 + + .../user/tasks/test_create_user_home.yml | 86 +++++++++++++++++++ + 4 files changed, 92 insertions(+), 1 deletion(-) + create mode 100644 changelogs/fragments/user_action_fix.yml + create mode 100644 test/integration/targets/user/files/skel/.ssh/known_hosts + +diff --git a/changelogs/fragments/user_action_fix.yml b/changelogs/fragments/user_action_fix.yml +new file mode 100644 +index 00000000..64ee997d +--- /dev/null ++++ b/changelogs/fragments/user_action_fix.yml +@@ -0,0 +1,2 @@ ++bugfixes: ++ - user module now avoids changing ownership of files symlinked in provided home dir skeleton +diff --git a/lib/ansible/modules/system/user.py b/lib/ansible/modules/system/user.py +index fd56fc68..9d47425f 100644 +--- a/lib/ansible/modules/system/user.py ++++ b/lib/ansible/modules/system/user.py +@@ -1154,7 +1154,9 @@ class User(object): + for d in dirs: + os.chown(os.path.join(root, d), uid, gid) + for f in files: +- os.chown(os.path.join(root, f), uid, gid) ++ full_path = os.path.join(root, f) ++ if not os.path.islink(full_path): ++ os.chown(full_path, uid, gid) + except OSError as e: + self.module.exit_json(failed=True, msg="%s" % to_native(e)) + +diff --git a/test/integration/targets/user/files/skel/.ssh/known_hosts b/test/integration/targets/user/files/skel/.ssh/known_hosts +new file mode 100644 +index 00000000..f5b72a21 +--- /dev/null ++++ b/test/integration/targets/user/files/skel/.ssh/known_hosts +@@ -0,0 +1 @@ ++test file, not real ssh hosts file +diff --git a/test/integration/targets/user/tasks/test_create_user_home.yml b/test/integration/targets/user/tasks/test_create_user_home.yml +index 1b529f76..3bc1023b 100644 +--- a/test/integration/targets/user/tasks/test_create_user_home.yml ++++ b/test/integration/targets/user/tasks/test_create_user_home.yml +@@ -134,3 +134,89 @@ + name: randomuser + state: absent + remove: yes ++ ++- name: Create user home directory with /dev/null as skeleton, https://github.com/ansible/ansible/issues/75063 ++ # create_homedir is mostly used by linux, rest of OSs take care of it themselves via -k option (which fails this task) ++ # OS X actuall breaks since it does not implement getpwnam() ++ when: ansible_system == 'Linux' ++ block: ++ - name: "Create user home directory with /dev/null as skeleton" ++ user: ++ name: withskeleton ++ state: present ++ skeleton: "/dev/null" ++ createhome: yes ++ register: create_user_with_skeleton_dev_null ++ always: ++ - name: "Remove test user" ++ user: ++ name: withskeleton ++ state: absent ++ remove: yes ++ ++- name: Create user home directory with skel that contains symlinks ++ tags: symlink_home ++ when: ansible_system == 'Linux' ++ become: True ++ vars: ++ flag: '{{tempdir.path}}/root_flag.conf' ++ block: ++ - name: make tempdir for skel ++ tempfile: state=directory ++ register: tempdir ++ ++ - name: create flag file ++ file: path={{flag}} owner=root state=touch ++ ++ - name: copy skell to target ++ copy: ++ dest: '{{tempdir.path}}/skel' ++ src: files/skel ++ register: skel ++ ++ - name: create the bad symlink ++ file: ++ src: '{{flag}}' ++ dest: '{{tempdir.path}}/skel/should_not_change_own' ++ state: link ++ ++ - name: "Create user home directory with skeleton" ++ user: ++ name: withskeleton ++ state: present ++ skeleton: "{{tempdir.path}}/skel" ++ createhome: yes ++ home: /home/missing/withskeleton ++ register: create_user_with_skeleton_symlink ++ ++ - name: Check flag ++ stat: path={{flag}} ++ register: test_flag ++ ++ - name: ensure we didn't change owner for flag ++ assert: ++ that: ++ - test_flag.stat.uid != create_user_with_skeleton_symlink.uid ++ ++ always: ++ - name: "Remove test user" ++ user: ++ name: withskeleton ++ state: absent ++ remove: yes ++ ++ - name: get files to delete ++ find: path="{{tempdir.path}}" ++ register: remove ++ when: ++ - tempdir is defined ++ - tempdir is success ++ ++ - name: "Remove temp files" ++ file: ++ path: '{{item}}' ++ state: absent ++ loop: "{{remove.files|default([])}}" ++ when: ++ - remove is success ++ +-- +2.47.0 + diff --git a/ansible.spec b/ansible.spec index d9e1e6fa902517bddd56585d61528c50f0f3d73d..518cfdee163d602389dc00942b0ebfb9828b6f23 100644 --- a/ansible.spec +++ b/ansible.spec @@ -2,7 +2,7 @@ Name: ansible Summary: SSH-based configuration management, deployment, and task execution system Version: 2.9.27 -Release: 5 +Release: 6 License: GPLv3+ Source0: https://releases.ansible.com/ansible/%{name}-%{version}.tar.gz @@ -19,6 +19,8 @@ Patch4: ansible-2.9.23-sphinx4.patch Patch5: hostname-module-support-openEuler.patch Patch6: Fix-build-error-for-sphinx-7.0.patch Patch7: CVE-2024-0690.patch +Patch8: CVE-2024-8775.patch +Patch9: CVE-2024-9902.patch Provides: ansible-python3 = %{version}-%{release} Obsoletes: ansible-python3 < %{version}-%{release} @@ -216,6 +218,9 @@ make PYTHON=/usr/bin/python3 tests-py3 %{python3_sitelib}/ansible_test %changelog +* Mon Dec 02 2024 yaoxin - 2.9.27-6 +- Fix CVE-2024-8775 and CVE-2024-9902 + * Mon Feb 05 2024 wangkai <13474090681@163.com> - 2.9.27-5 - Fix CVE-2024-0690