Fixes the symlink handling issue in the authorized_key module (#760) (#762)

- Addresses CVE-2026-11837
- Fixes #759

Signed-off-by: Hideki Saito <saito@fgrep.org>
This commit is contained in:
Hideki Saito
2026-07-01 12:30:27 +09:00
committed by GitHub
parent ee1461f46c
commit a4805b92b7
6 changed files with 105 additions and 9 deletions

View 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).

View File

@@ -307,6 +307,17 @@ class keydict(dict):
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):
"""
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)))
if module.selinux_enabled():
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))
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):
os.makedirs(basedir)
f = None
try:
f = open(keysfile, "w") # touches file so we can set ownership and perms
finally:
f.close()
fd = _safe_open_write(module, keysfile, follow)
os.close(fd)
if module.selinux_enabled():
module.set_default_selinux_context(keysfile, False)
try:
os.chown(keysfile, uid, gid)
os.chown(keysfile, uid, gid, follow_symlinks=follow)
os.chmod(keysfile, int('0600', 8))
except OSError:
pass
@@ -607,7 +615,7 @@ def enforce_state(module, params):
# check current state -- just get the filename, don't create file
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_keys = parsekeys(module, existing_content)
@@ -696,6 +704,11 @@ def enforce_state(module, params):
if not module.check_mode:
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
return params

View File

@@ -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
...

View File

@@ -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
...

View File

@@ -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
...

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# 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
- name: Test for multiple keys handling
@@ -37,3 +37,12 @@
- name: Test for permission denied files
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