## Summary

CVE-2026-44901 is a Wazuh cluster Distributed API (DAPI) deserialization vulnerability in which a malicious or compromised worker node can return a crafted serialized `AffectedItemsWazuhResult` to a master node. In vulnerable code, `AffectedItemsWazuhResult.decode_json()` accepts attacker-controlled `sort_casting` values and `merge()` resolves those values with `getattr(builtins, type_)`. A worker response containing `sort_casting=["exec"]` therefore makes the master call Python `exec()` on attacker-controlled item data while merging results from multiple nodes.

## Impact

- **Affected package/component:** Wazuh manager cluster framework, specifically `framework/wazuh/core/results.py` as reached through `framework/wazuh/core/cluster/dapi/dapi.py` and the cluster TCP channel handled by `wazuh.core.cluster.master.MasterHandler` / `wazuh.core.cluster.common.Handler`.
- **Affected versions:** Wazuh manager cluster deployments before the fix commit `b29849f8abb08d78f257e6106b6111a8a1b0e621` (reported as fixed in 4.14.6 and later). The reproduced vulnerable revision is the fixed commit parent: `24609e140155d7fd2bddd4ebb045dbde5bea320f`.
- **Risk level and consequences:** High. A malicious/compromised worker node, or an attacker with the shared cluster key able to act as a worker on the cluster channel, can cause code/command execution in the master-side Wazuh process when a distributed API response is merged.

## Impact Parity

- **Disclosed/claimed maximum impact:** Code execution on the Wazuh cluster master via the TCP/1516 Fernet-encrypted cluster channel and DAPI result merging.
- **Reproduced impact from this run:** Code execution/command execution on the master-side Wazuh process. The payload executed via `exec()` and wrote unique marker files during both vulnerable attempts.
- **Parity:** `full`
- **Not demonstrated:** No additional privilege escalation beyond the privileges of the reproduced master-side process was claimed or required for this proof. The proof uses a minimally configured product cluster runtime rather than full Dockerized Wazuh service containers because Docker is unavailable in this environment, but it exercises the original Wazuh cluster TCP/Fernet framing, `MasterHandler`, `DistributedAPI.forward_request`, `json.loads(..., object_hook=as_wazuh_object)`, and `results.py` merge sink.

## Root Cause

The root cause is unsafe deserialization and later use of trusted-as-code type names from a worker-provided JSON result object.

In the vulnerable commit `24609e140155d7fd2bddd4ebb045dbde5bea320f`:

- `AffectedItemsWazuhResult.decode_json()` in `framework/wazuh/core/results.py` stores `obj['sort_casting']` directly into the result object.
- During DAPI result merging, `AffectedItemsWazuhResult.__or__()` calls `merge(..., types=self.sort_casting)`.
- `merge()` constructs casters using `getattr(builtins, type_)` without an allowlist.
- `_goes_before_than()` applies each caster to sort values. If the worker supplied `sort_casting=["exec"]`, the caster becomes Python built-in `exec`, and the corresponding item value is executed as Python source.

The product path that reaches this is:

1. Master forwards a distributed request to the worker through `MasterHandler.execute(command=b'dapi_fwd', ...)`.
2. The worker response is delivered through the original Wazuh cluster string protocol (`new_str`, `str_upd`, `dapi_res`) over the Fernet-encrypted cluster TCP channel.
3. `dapi.py` parses the worker response with `json.loads(..., object_hook=c_common.as_wazuh_object)`.
4. `as_wazuh_object()` calls `AffectedItemsWazuhResult.decode_json()`.
5. With more than one node response, `dapi.py` merges results using `reduce(or_, response)`, triggering `results.py` sorting/casting.

The fix commit is:

- `b29849f8abb08d78f257e6106b6111a8a1b0e621`

That commit adds validation in `decode_json()` and an explicit `ALLOWED_CASTERS` map in `merge()`, allowing only `int`, `float`, `str`, and `bool`. The same malicious `sort_casting=["exec"]` response is rejected with `WazuhInternalError: Invalid sort_casting type 'exec'. Allowed types: bool, float, int, str`.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh`.
2. The script:
   - Reuses the prepared Wazuh repository at `/pruva/project-cache/repo` when available.
   - Resolves the fixed commit and vulnerable parent.
   - Verifies the vulnerable revision still contains `getattr(builtins, type_)` and the fixed revision contains the new `sort_casting` allowlist.
   - Creates worktrees for `24609e140155d7fd2bddd4ebb045dbde5bea320f` and `b29849f8abb08d78f257e6106b6111a8a1b0e621`.
   - Starts a minimally configured real Wazuh master cluster TCP listener using `wazuh.core.cluster.master.MasterHandler` and `wazuh.core.cluster.common.Handler` with Fernet enabled. It binds to `127.0.0.1:1516` when available.
   - Connects a malicious worker peer over the original Wazuh cluster frame format, completes the encrypted `hello` handshake, receives the master `b'dapi'` forwarded request, and returns the crafted JSON through the original `new_str` / `str_upd` / `dapi_res` sequence.
   - Runs two vulnerable attempts and two fixed attempts.
3. Expected evidence:
   - Vulnerable attempts produce `repro/markers/product_marker_vuln_1.txt` and `repro/markers/product_marker_vuln_2.txt`, each containing the unique marker value selected for that process.
   - Fixed attempts reach the same DAPI/object-hook path but reject `sort_casting='exec'` and do not create marker files.
   - `bundle/repro/validation_verdict.json` reports `claim_outcome=confirmed`, `validated_surface=network_protocol`, `evidence_scope=production_path`, and `observed_impact_class=code_execution`.

## Evidence

Primary runtime artifacts are listed and digest-bound in `bundle/repro/runtime_manifest.json`.

Key files from the final successful run:

- `bundle/logs/product_patch_check.log` — commit identity and patch absence/presence check.
- `bundle/logs/product_vuln_1.log` and `bundle/logs/product_vuln_2.log` — vulnerable product-path attempts.
- `bundle/logs/product_fixed_1.log` and `bundle/logs/product_fixed_2.log` — fixed negative-control attempts.
- `bundle/repro/observations/product_vuln_1.json` and `bundle/repro/observations/product_vuln_2.json` — structured observations showing `fernet_enabled=true`, `master_listened=true`, `dapi_forward_request_received_by_worker=true`, `worker_response_delivered_via_send_string=true`, `dapi_json_object_hook_path_reached=true`, and `marker_present=true`.
- `bundle/repro/observations/product_fixed_1.json` and `bundle/repro/observations/product_fixed_2.json` — structured observations showing the same network/DAPI path was reached but `sort_casting_rejected=true` and `marker_present=false`.
- `bundle/repro/markers/product_marker_vuln_1.txt` and `bundle/repro/markers/product_marker_vuln_2.txt` — command-execution markers written by the vulnerable master-side process.
- `bundle/repro/evil_worker_response.json` — the attacker-controlled worker JSON response containing `sort_casting=["exec"]` and the payload in `affected_items[*].x`.

Representative vulnerable evidence from the product logs:

- `MASTER_LISTENING original_wazuh_cluster_tcp=127.0.0.1:1516 ... fernet_key_len=32`
- `WORKER_HELLO_ACCEPTED response=b'Client worker01 added'`
- `WORKER_GOT_DAPI_REQUEST request_id=... json_len=550 ...`
- `WORKER_SEND_STRING_UPDATED malicious JSON stored in master MasterHandler.in_str`
- `WORKER_DAPI_RES_ACKNOWLEDGED master accepted dapi_res and released pending DistributedAPI request`
- `MASTER_DAPI_RESULT type=AffectedItemsWazuhResult ... '_sort_casting': ['exec'] ...`
- `MARKER_CHECK ... present=True content='CVE-2026-44901-vuln-...'`

Representative fixed evidence:

- `MASTER_DAPI_RESULT type=WazuhInternalError ... "Invalid sort_casting type 'exec'. Allowed types: bool, float, int, str"`
- `MARKER_CHECK ... present=False content=None`

Environment details captured:

- Repository URL: `https://github.com/wazuh/wazuh.git`
- Vulnerable commit: `24609e140155d7fd2bddd4ebb045dbde5bea320f`
- Fixed commit: `b29849f8abb08d78f257e6106b6111a8a1b0e621`
- Entrypoint: Wazuh cluster TCP peer (`entrypoint_kind="tcp_peer"`)
- Runtime stack: Wazuh `MasterHandler`, Wazuh Fernet frame `Handler`, Wazuh `DistributedAPI.forward_request`, Wazuh `AffectedItemsWazuhResult.merge`, Python 3

## Recommendations / Next Steps

- Use the fixed implementation from `b29849f8abb08d78f257e6106b6111a8a1b0e621` or upgrade to Wazuh 4.14.6 or later.
- Keep `sort_casting` validation both at deserialization time and at use time. Only safe, explicit caster names should be accepted.
- Avoid resolving attacker-provided strings into arbitrary built-ins or callables.
- Add regression tests that replay serialized worker `AffectedItemsWazuhResult` responses with invalid caster names such as `exec`, `eval`, `open`, and non-string/list values.
- Consider hardening the DAPI worker-response trust boundary: even authenticated cluster workers should not be able to deserialize data structures that influence executable behavior on the master.

## Additional Notes

- The final `bundle/repro/reproduction_steps.sh` was executed twice consecutively and succeeded both times.
- Each script run performs two vulnerable attempts and two fixed attempts with fresh per-process marker values.
- Docker was not available in this environment, so the proof uses a minimally configured local Wazuh cluster runtime from the real source tree rather than Wazuh container images. It does not reimplement the vulnerable sink or the cluster frame protocol; it imports and executes the real Wazuh modules for the master TCP listener, Fernet framing, DAPI forwarding, JSON object hook, and result merging.
- The reproduction is self-contained: it creates runtime helper code and payload files at execution time, installs Python dependencies into `bundle/repro/venv` if needed, and writes all proof diagnostics under `bundle/logs/` and `bundle/repro/`.
