# RCA Report: CVE-2026-7474

## Summary

CVE-2026-7474 is a path traversal vulnerability in HashiCorp Nomad's dynamic host volume plugin loader that allows arbitrary command execution on Nomad client nodes. When an authenticated user with `host-volume-create` ACL capability submits a `HostVolume.Create` request with a malicious `PluginID` containing directory traversal sequences (e.g., `../../../../bin/ls`) and an explicit `NodeID`, the server bypasses plugin feasibility checks and forwards the request to the target client. The client's `NewHostVolumePluginExternal` function uses `filepath.Join(pluginDir, filename)` followed by `os.Stat`, which resolves the traversal to an arbitrary executable outside the configured plugin directory. That executable is later invoked via `exec.CommandContext` during volume `Create`, `Fingerprint`, or `Delete` operations.

## Impact

- **Package/component affected:** `github.com/hashicorp/nomad/client/hostvolumemanager` (`host_volume_plugin.go`), `github.com/hashicorp/nomad/nomad` (`host_volume_endpoint.go`)
- **Affected versions:** Prior to `v2.0.1` (confirmed vulnerable in `v2.0.0`, `v1.10.5`, `v1.11.2`)
- **Fixed versions:** `v2.0.1`
- **Risk level:** High (CVSS 3.1: 8.8)
- **Consequences:** Authenticated attackers with namespace-level `host-volume-create` ACLs can force any Nomad client node to execute arbitrary binaries from the host filesystem as the Nomad client user, leading to full client-node compromise.

## Root Cause

The vulnerability stems from two missing validations in the host volume create workflow:

1. **Client-side path traversal (CWE-22):** `NewHostVolumePluginExternal` in `client/hostvolumemanager/host_volume_plugin.go` (v2.0.0, ~line 224) constructs the plugin executable path with `filepath.Join(pluginDir, filename)` and then calls `os.Stat(executable)`. Because `filepath.Join` does not reject `../` sequences, a malicious `PluginID` resolves to any path on the filesystem. If the target file exists and is executable, the client stores the escaped path and later invokes it via `exec.CommandContext`.

2. **Server-side feasibility bypass:** In `nomad/host_volume_endpoint.go`, the `placeHostVolume` function skips the plugin feasibility constraint (`${attr.plugins.host_volume.<plugin>.version} is_set`) when the user explicitly provides a `NodeID`. This was intended as an optimization, but it allows an attacker to force placement onto a node that does not legitimately advertise the plugin, bypassing the only server-side guard that could block the malicious request.

The fix commit (`cd7240c4099ad33eda279924fb3a9459b162d120`, released in `v2.0.1`) addresses both issues:
- **Client-side:** Replaces `filepath.Join` + `os.Stat` with `os.OpenRoot(pluginDir)` and `root.Stat(filename)`. On Go 1.24+, `os.Root.Stat` rejects paths that escape the root directory, returning an error that is mapped to `ErrPluginNotExists`.
- **Server-side:** Moves the plugin feasibility constraint so it is checked **even when the request already specifies an explicit `NodeID`**, preventing traversal payloads from being forwarded to the client.

## Reproduction Steps

The reproduction script is `repro/reproduction_steps.sh`. It performs the following steps:

1. Clones the HashiCorp Nomad repository (if not already present) and checks out both `v2.0.0` (vulnerable) and `v2.0.1` (fixed).
2. Builds the `nomad` binary for each tag (`bin/nomad-vuln` and `bin/nomad-fixed`).
3. Writes a minimal `nomad agent -dev` configuration that enables both server and client roles, binds to `127.0.0.1`, and sets a `host_volume_plugin_dir`.
4. Starts the vulnerable agent, waits for it to be healthy, and queries its NodeID via `/v1/nodes`.
5. Sends a malicious `PUT /v1/volume/host/create` request with:
   - `PluginID: "../../../../bin/ls"`
   - Explicit `NodeID`
   - Minimal capacity constraints
6. Captures the HTTP response body and code into `logs/vulnerable_api.txt`.
7. Stops the vulnerable agent, frees ports, and repeats steps 4–6 with the fixed binary.
8. Captures the fixed response into `logs/fixed_api.txt`.
9. Writes `repro/runtime_manifest.json` with both results.
10. Verifies that the vulnerable run shows evidence of `/bin/ls` execution (`exit status 2`) while the fixed run shows a server-side placement rejection (`node ... is not feasible for volume`).

### Expected evidence

- **Vulnerable (`v2.0.0`):** HTTP 500 response containing:
  `HostVolume.Create error: error creating volume "<id>" with plugin "../../../../bin/ls": exit status 2`
  This proves the server forwarded the request, the client resolved the traversal to `/bin/ls`, and executed `/bin/ls create` (which exits with code 2 because `ls` does not understand the `create` subcommand).

- **Fixed (`v2.0.1`):** HTTP 500 response containing:
  `could not place volume "cve-test-vol": node <id> is not feasible for volume`
  This proves the server now enforces the plugin feasibility constraint even with an explicit `NodeID`, rejecting the request before it ever reaches the client.

## Evidence

- `logs/vulnerable_api.txt` — API response from the vulnerable agent run.
- `logs/fixed_api.txt` — API response from the fixed agent run.
- `repro/runtime_manifest.json` — Structured manifest with payload, endpoint, and captured HTTP codes for both versions.
- `external/nomad/bin/nomad-vuln` and `external/nomad/bin/nomad-fixed` — Compiled Nomad binaries used during reproduction.

### Key excerpts

Vulnerable response (`logs/vulnerable_api.txt`):
```
HostVolume.Create error: error creating volume "f09840bb-a5d8-0b96-39c6-77deafd35c19" with plugin "../../../../bin/ls": exit status 2
HTTP_CODE:500
```

Fixed response (`logs/fixed_api.txt`):
```
could not place volume "cve-test-vol": node 33225465-afa9-a5c1-2acf-02c0de00879a is not feasible for volume
HTTP_CODE:500
```

## Recommendations / Next Steps

1. **Upgrade immediately:** All Nomad deployments should be upgraded to `v2.0.1` or later. The fix includes both client-side `os.OpenRoot` containment and server-side feasibility enforcement.
2. **ACL hardening:** Restrict `host-volume-create` capabilities to the smallest set of namespaces and users that genuinely require it. Even with the fix, the capability is powerful.
3. **Network segmentation:** Place Nomad client agents on isolated networks so that even a compromised client cannot easily pivot to other infrastructure.
4. **Monitoring:** Alert on host volume create requests that reference unusual `PluginID` values (e.g., containing `..`, `/`, or absolute paths) as a defense-in-depth measure.
5. **Testing:** Add regression tests that verify `os.Root.Stat` rejects traversal sequences and that `placeHostVolume` always checks plugin feasibility regardless of whether `NodeID` is explicit.

## Additional Notes

- **Idempotency:** The reproduction script was executed twice consecutively and produced identical differential behavior on both runs (vulnerable → `exit status 2`, fixed → `not feasible`).
- **Environment:** Reproduction performed in an Ubuntu container with Go 1.24.7. A cgroup workaround (`mount -t tmpfs` over `/sys/fs/cgroup/cpuset`) was required because the container lacked the `cpuset.mems` file expected by Nomad's cgroup initialization.
- **Limitations:** The reproduction demonstrates the traversal through the actual HTTP API and real agent binaries. The `exit status 2` artifact proves command execution of the escaped path. A fully successful volume creation would require a compliant plugin executable; using `/bin/ls` intentionally produces a failure that still proves the vulnerability.
