Bug 2544510 (CVE-2026-103754)

Summary: CVE-2026-103754 ansible-runner: ansible-runner: path traversal and symlink escape in unstream_dir() allows file write outside the target directory
Product: [Other] Security Response Reporter: OSIDB Bzimport <bzimport>
Component: vulnerabilityAssignee: Product Security <prodsec-ir-bot>
Status: NEW --- QA Contact:
Severity: medium Docs Contact:
Priority: medium    
Version: unspecifiedCC: dschmidt, ikhan, jlanda, kshier, rbobbitt, simaishi, stcannon, yguenane
Target Milestone: ---Keywords: Security
Target Release: ---   
Hardware: All   
OS: Linux   
Whiteboard:
Fixed In Version: Doc Type: ---
Doc Text:
A flaw was found in ansible-runner. The unstream_dir() function, which receives and extracts a streamed zip archive on the worker side of the ansible-runner transmit/worker protocol, re-creates symbolic links from archive content without validating the link target and applies chmod() and utime() to an unsanitized filesystem path derived from the archive member name. A crafted archive processed by a worker that consumes attacker-influenced input can create files, create symbolic links, or change permissions outside the intended target directory, which can be leveraged toward code execution.
Story Points: ---
Clone Of: Environment:
Last Closed: Type: ---
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:
Embargoed:

Description OSIDB Bzimport 2026-10-01 10:39:11 UTC
Reported via the ansible-security list by Liqiang Ji (ISCAS / SQUARE Research Group).

Module: src/ansible_runner/utils/streaming.py, function unstream_dir().

unstream_dir() performs a custom ("fancy") extraction to preserve permissions and symlinks. Two defects:
1. CWE-59 (link following): a symlink member's target is read verbatim from the archive content and re-created with os.symlink() without validation, so it can point outside target_directory (e.g. '../outside' or an absolute path). A subsequent member extracted through that symlink lands outside target_directory (arbitrary write escape).
2. CWE-22 (path traversal): out_path = os.path.join(target_directory, info.filename) is built from the unsanitized member name. ZipFile.extract() sanitizes the arcname, but os.utime()/os.chmod() operate on the raw out_path, so a member named '../victim' changes mtime/permissions on a file outside target_directory.

Exploitability: in the standard AWX/AAP topology the transmit stream is produced by the trusted controller (stream_dir only encodes pre-existing symlinks) and the controller->executor path is authenticated, so there this is defense-in-depth hardening. It is directly relevant to deployments that feed untrusted input into ansible-runner's Worker() path.

Upstream: https://github.com/ansible/ansible-runner
Fix PR: https://github.com/ansible/ansible-runner/pull/1550
Confirmed present in 2.4.3 and devel.