mirror of
https://github.com/ansible-collections/ansible.posix.git
synced 2026-07-25 00:44:46 +00:00
Fixes the symlink handling issue in the authorized_key module (#760)
- Addresses CVE-2026-11837 - Fixes #759 Signed-off-by: Hideki Saito <saito@fgrep.org>
This commit is contained in:
2
changelogs/fragments/759_cve_2026_11837.yml
Normal file
2
changelogs/fragments/759_cve_2026_11837.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
security_fixes:
|
||||||
|
- authorized_key - fix local privilege escalation via symlink-following when running as root (https://github.com/ansible-collections/ansible.posix/issues/759).
|
||||||
@@ -307,6 +307,17 @@ class keydict(dict):
|
|||||||
return [item[1] for item in self.items()]
|
return [item[1] for item in self.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_open_write(module, path, follow):
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||||
|
if not follow and hasattr(os, 'O_NOFOLLOW'):
|
||||||
|
flags |= os.O_NOFOLLOW
|
||||||
|
try:
|
||||||
|
fd = os.open(path, flags, int('0600', 8))
|
||||||
|
except OSError as e:
|
||||||
|
module.fail_json(msg="File open failed %s : %s" % (path, to_native(e)))
|
||||||
|
return fd
|
||||||
|
|
||||||
|
|
||||||
def keyfile(module, user, write=False, path=None, manage_dir=True, follow=False):
|
def keyfile(module, user, write=False, path=None, manage_dir=True, follow=False):
|
||||||
"""
|
"""
|
||||||
Calculate name of authorized keys file, optionally creating the
|
Calculate name of authorized keys file, optionally creating the
|
||||||
@@ -359,7 +370,7 @@ def keyfile(module, user, write=False, path=None, manage_dir=True, follow=False)
|
|||||||
module.fail_json(msg="Failed to create directory %s : %s" % (sshdir, to_native(e)))
|
module.fail_json(msg="Failed to create directory %s : %s" % (sshdir, to_native(e)))
|
||||||
if module.selinux_enabled():
|
if module.selinux_enabled():
|
||||||
module.set_default_selinux_context(sshdir, False)
|
module.set_default_selinux_context(sshdir, False)
|
||||||
os.chown(sshdir, uid, gid)
|
os.chown(sshdir, uid, gid, follow_symlinks=follow)
|
||||||
os.chmod(sshdir, int('0700', 8))
|
os.chmod(sshdir, int('0700', 8))
|
||||||
|
|
||||||
if not os.path.exists(keysfile):
|
if not os.path.exists(keysfile):
|
||||||
@@ -367,16 +378,13 @@ def keyfile(module, user, write=False, path=None, manage_dir=True, follow=False)
|
|||||||
if not os.path.exists(basedir):
|
if not os.path.exists(basedir):
|
||||||
os.makedirs(basedir)
|
os.makedirs(basedir)
|
||||||
|
|
||||||
f = None
|
fd = _safe_open_write(module, keysfile, follow)
|
||||||
try:
|
os.close(fd)
|
||||||
f = open(keysfile, "w") # touches file so we can set ownership and perms
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
if module.selinux_enabled():
|
if module.selinux_enabled():
|
||||||
module.set_default_selinux_context(keysfile, False)
|
module.set_default_selinux_context(keysfile, False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.chown(keysfile, uid, gid)
|
os.chown(keysfile, uid, gid, follow_symlinks=follow)
|
||||||
os.chmod(keysfile, int('0600', 8))
|
os.chmod(keysfile, int('0600', 8))
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
@@ -607,7 +615,7 @@ def enforce_state(module, params):
|
|||||||
|
|
||||||
# check current state -- just get the filename, don't create file
|
# check current state -- just get the filename, don't create file
|
||||||
do_write = False
|
do_write = False
|
||||||
params["keyfile"] = keyfile(module, user, do_write, path, manage_dir)
|
params["keyfile"] = keyfile(module, user, do_write, path, manage_dir, follow)
|
||||||
existing_content = readfile(module, params["keyfile"])
|
existing_content = readfile(module, params["keyfile"])
|
||||||
existing_keys = parsekeys(module, existing_content)
|
existing_keys = parsekeys(module, existing_content)
|
||||||
|
|
||||||
@@ -696,6 +704,11 @@ def enforce_state(module, params):
|
|||||||
|
|
||||||
if not module.check_mode:
|
if not module.check_mode:
|
||||||
writefile(module, filename, new_content)
|
writefile(module, filename, new_content)
|
||||||
|
user_entry = pwd.getpwnam(user)
|
||||||
|
uid = user_entry.pw_uid
|
||||||
|
gid = user_entry.pw_gid
|
||||||
|
os.chown(filename, uid, gid, follow_symlinks=follow)
|
||||||
|
os.chmod(filename, int('0600', 8))
|
||||||
params['changed'] = True
|
params['changed'] = True
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
#
|
||||||
|
# Check: keysfile is symlink and follow=false
|
||||||
|
#
|
||||||
|
- name: Try to add key with keysfile as symlink
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: testuser
|
||||||
|
key: "{{ rsa_key_basic }}"
|
||||||
|
state: present
|
||||||
|
manage_dir: false
|
||||||
|
follow: false
|
||||||
|
|
||||||
|
- name: Assert target file ownership unchanged
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /tmp/symlink_test_target/target_file
|
||||||
|
register: target_file_stat
|
||||||
|
|
||||||
|
- name: Verify target file is still owned by root
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- target_file_stat.stat.uid == 0
|
||||||
|
...
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Remove testuser
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: testuser
|
||||||
|
state: absent
|
||||||
|
remove: true
|
||||||
|
|
||||||
|
- name: Remove symlink target directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /tmp/symlink_test_target
|
||||||
|
state: absent
|
||||||
|
...
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
- name: Create testuser for symlink tests
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: testuser
|
||||||
|
create_home: true
|
||||||
|
register: testuser_result
|
||||||
|
|
||||||
|
- name: Create symlink target directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /tmp/symlink_test_target
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
- name: Create a target file owned by root
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /tmp/symlink_test_target/target_file
|
||||||
|
content: "sensitive data"
|
||||||
|
owner: root
|
||||||
|
mode: '0600'
|
||||||
|
|
||||||
|
- name: Remove .ssh directory if exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ testuser_result.home }}/.ssh"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Create .ssh directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ testuser_result.home }}/.ssh"
|
||||||
|
state: directory
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
- name: Create symlink from authorized_keys to target file
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: /tmp/symlink_test_target/target_file
|
||||||
|
dest: "{{ testuser_result.home }}/.ssh/authorized_keys"
|
||||||
|
state: link
|
||||||
|
...
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
- name: Setup testing environment
|
- name: Setup for testing environment
|
||||||
ansible.builtin.import_tasks: setup_steps.yml
|
ansible.builtin.import_tasks: setup_steps.yml
|
||||||
|
|
||||||
- name: Test for multiple keys handling
|
- name: Test for multiple keys handling
|
||||||
@@ -37,3 +37,12 @@
|
|||||||
|
|
||||||
- name: Test for permission denied files
|
- name: Test for permission denied files
|
||||||
ansible.builtin.import_tasks: check_permissions.yml
|
ansible.builtin.import_tasks: check_permissions.yml
|
||||||
|
|
||||||
|
- name: CVE-2026-11837 Setup for symlink tests
|
||||||
|
ansible.builtin.import_tasks: check_symlink_setup.yml
|
||||||
|
|
||||||
|
- name: CVE-2026-11837 Test for symlink tests
|
||||||
|
ansible.builtin.import_tasks: check_symlink.yml
|
||||||
|
|
||||||
|
- name: CVE-2026-11837 Cleanup symlink test
|
||||||
|
ansible.builtin.import_tasks: check_symlink_cleanup.yml
|
||||||
|
|||||||
Reference in New Issue
Block a user