CVE-2026-7474: Verified Repro With Script Download
CVE-2026-7474: HashiCorp Nomad: path traversal in host volume plugin loader → client-host RCE
CVE-2026-7474 is verified against nomad · go. Affected versions: prior to v2.0.1 (confirmed at v2.0.0, v1.10.5, v1.11.2). Fixed in v2.0.1. Vulnerability class: RCE. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00185.
What Is CVE-2026-7474?
CVE-2026-7474 (GHSA-hx53-77qj-8663) is a high-severity path traversal vulnerability in HashiCorp Nomad's dynamic host volume plugin loader that allows arbitrary command execution on Nomad client nodes. Pruva reproduced it (reproduction REPRO-2026-00185).
CVE-2026-7474 Severity & CVSS Score
CVE-2026-7474 is rated high severity, with a CVSS base score of 8.8 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected nomad Versions
nomad · go versions prior to v2.0.1 (confirmed at v2.0.0, v1.10.5, v1.11.2) are affected.
How to Reproduce CVE-2026-7474
pruva-verify REPRO-2026-00185 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00185/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-7474
Reproduced by Pruva's autonomous agents — 337 tool calls over 48 min. Full root-cause analysis and the complete transcript are below.
Variant analysis for CVE-2026-7474: tested path traversal, symlink escape, and Register->Delete chain against the fixed version v2.0.1. No bypass confirmed. Client-side os.OpenRoot blocks all direct traversal variants.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-7474
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.
- 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 inv2.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-createACLs 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:
Client-side path traversal (CWE-22):
NewHostVolumePluginExternalinclient/hostvolumemanager/host_volume_plugin.go(v2.0.0, ~line 224) constructs the plugin executable path withfilepath.Join(pluginDir, filename)and then callsos.Stat(executable). Becausefilepath.Joindoes not reject../sequences, a maliciousPluginIDresolves to any path on the filesystem. If the target file exists and is executable, the client stores the escaped path and later invokes it viaexec.CommandContext.Server-side feasibility bypass: In
nomad/host_volume_endpoint.go, theplaceHostVolumefunction skips the plugin feasibility constraint (${attr.plugins.host_volume.<plugin>.version} is_set) when the user explicitly provides aNodeID. 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.Statwithos.OpenRoot(pluginDir)androot.Stat(filename). On Go 1.24+,os.Root.Statrejects paths that escape the root directory, returning an error that is mapped toErrPluginNotExists. - 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:
- Clones the HashiCorp Nomad repository (if not already present) and checks out both
v2.0.0(vulnerable) andv2.0.1(fixed). - Builds the
nomadbinary for each tag (bin/nomad-vulnandbin/nomad-fixed). - Writes a minimal
nomad agent -devconfiguration that enables both server and client roles, binds to127.0.0.1, and sets ahost_volume_plugin_dir. - Starts the vulnerable agent, waits for it to be healthy, and queries its NodeID via
/v1/nodes. - Sends a malicious
PUT /v1/volume/host/createrequest with:PluginID: "../../../../bin/ls"- Explicit
NodeID - Minimal capacity constraints
- Captures the HTTP response body and code into
logs/vulnerable_api.txt. - Stops the vulnerable agent, frees ports, and repeats steps 4–6 with the fixed binary.
- Captures the fixed response into
logs/fixed_api.txt. - Writes
repro/runtime_manifest.jsonwith both results. - Verifies that the vulnerable run shows evidence of
/bin/lsexecution (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 2This proves the server forwarded the request, the client resolved the traversal to/bin/ls, and executed/bin/ls create(which exits with code 2 becauselsdoes not understand thecreatesubcommand).Fixed (
v2.0.1): HTTP 500 response containing:could not place volume "cve-test-vol": node <id> is not feasible for volumeThis proves the server now enforces the plugin feasibility constraint even with an explicitNodeID, 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-vulnandexternal/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
- Upgrade immediately: All Nomad deployments should be upgraded to
v2.0.1or later. The fix includes both client-sideos.OpenRootcontainment and server-side feasibility enforcement. - ACL hardening: Restrict
host-volume-createcapabilities to the smallest set of namespaces and users that genuinely require it. Even with the fix, the capability is powerful. - Network segmentation: Place Nomad client agents on isolated networks so that even a compromised client cannot easily pivot to other infrastructure.
- Monitoring: Alert on host volume create requests that reference unusual
PluginIDvalues (e.g., containing..,/, or absolute paths) as a defense-in-depth measure. - Testing: Add regression tests that verify
os.Root.Statrejects traversal sequences and thatplaceHostVolumealways checks plugin feasibility regardless of whetherNodeIDis 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 tmpfsover/sys/fs/cgroup/cpuset) was required because the container lacked thecpuset.memsfile expected by Nomad's cgroup initialization. - Limitations: The reproduction demonstrates the traversal through the actual HTTP API and real agent binaries. The
exit status 2artifact proves command execution of the escaped path. A fully successful volume creation would require a compliant plugin executable; using/bin/lsintentionally produces a failure that still proves the vulnerability.
Variant Analysis & Alternative Triggers for CVE-2026-7474
CVE-2026-7474 is a path-traversal vulnerability in HashiCorp Nomad's dynamic host volume plugin loader. The original reproduction showed that a malicious PluginID containing ../ sequences could be passed through HostVolume.Create to the client, where filepath.Join(pluginDir, filename) resolved outside the plugin directory and allowed arbitrary code execution.
Variant analysis examined three distinct attack hypotheses against the fixed version (v2.0.1):
- Symlink bypass: A symlink inside the plugin directory pointing to an arbitrary executable outside.
- Register → Delete chain: Updating an existing volume's
PluginIDviaHostVolume.Register, then deleting it to trigger execution. - Path traversal with
..: The original payload tested directly againstNewHostVolumePluginExternal.
All three direct traversal variants were blocked on v2.0.1 by the client-side os.OpenRoot containment in NewHostVolumePluginExternal. No bypass of the patched code path was found. The Register endpoint remains a latent variant path because it does not validate PluginID, but it cannot be exploited because the client-side sink is hardened.
Fix Coverage / Assumptions
The fix (cd7240c4099ad33eda279924fb3a9459b162d120) relies on two invariants:
- Client-side
os.OpenRootis unbypassable.root.Stat(filename)uses Linuxopenat2withRESOLVE_IN_ROOT, which prevents..resolution, absolute paths, and symlink escapes from leaving the opened directory. Empirical testing with Go 1.24+ confirmed this blocks../,../../bin/ls, absolute paths, and symlinks pointing outside the root. - Server-side feasibility check blocks malicious requests before forwarding. Even when an explicit
NodeIDis provided,placeHostVolumenow enforces the${attr.plugins.host_volume.<plugin>.version} is_setconstraint, preventing traversal payloads from reaching the client via the normalHostVolume.Createpath.
The fix does not cover:
- The
HostVolume.Registerendpoint, which does not validatePluginIDfor path traversal and does not enforce plugin feasibility. - Agent restart via
restoreFromState, which loadsPluginIDfrom local disk state.
Variant / Alternate Trigger
Variant 1: Symlink escape via os.OpenRoot
- Hypothesis: A symlink placed inside the plugin directory could point to
/bin/ls. On v2.0.0,os.Statfollowed the symlink and found the target executable. On v2.0.1,root.StatwithRESOLVE_IN_ROOTblocks symlinks that escape the root. - Entry point: Same as original (
HostVolume.CreatewithPluginID = "symlink_plugin"). - Sink:
NewHostVolumePluginExternal→root.Stat→ blocked.
Variant 2: Register → Delete chain
- Hypothesis:
HostVolume.Registerallows updating an existing volume'sPluginIDbecauseValidateUpdatedoes not checkPluginIDchanges and Register does not callplaceHostVolume. After updating thePluginIDto a traversal payload,HostVolume.Deleteforwards the storedPluginIDto the client. - Entry point:
HostVolume.Registerfollowed byHostVolume.Delete. - Sink:
HostVolumeManager.Delete→getPlugin→NewHostVolumePluginExternal→ blocked on v2.0.1 byos.OpenRoot.
Variant 3: Direct traversal payload
Hypothesis: Directly calling
NewHostVolumePluginExternalwithPluginID = "../../../../bin/ls".Entry point: Any RPC path that reaches
getPlugin(Create, Delete, restoreFromState).Sink:
NewHostVolumePluginExternal→ blocked on v2.0.1.Package/component:
github.com/hashicorp/nomad/client/hostvolumemanagerAffected versions: v2.0.0 and earlier (vulnerable to variants); v2.0.1 (blocked)
Risk level on vulnerable version: High — arbitrary command execution as Nomad client user
Risk level on fixed version: None — variants are blocked at the client sink
Root Cause
The underlying root cause is the same as the original CVE: NewHostVolumePluginExternal resolves a user-supplied filename against a filesystem directory without constraining the resolution to that directory. The fix replaces filepath.Join + os.Stat with os.OpenRoot + root.Stat, which enforces the containment invariant at the kernel level via RESOLVE_IN_ROOT.
The Register endpoint is a latent variant path because it lacks the same input validation on PluginID, but it cannot be exploited on the fixed version because the client sink is hardened.
Reproduction Steps
- Run
bash vuln_variant/reproduction_steps.sh - The script:
- Creates Git worktrees for
v2.0.0(vulnerable) andv2.0.1(fixed) - Copies a Go test (
variant_test_test.go) into both worktrees - Runs
go test -run TestVariantAttemptson both - Compares results
- Creates Git worktrees for
- Expected evidence:
- On v2.0.0:
Variant 1 (traversal): err = <nil>andVariant 2 (symlink): err = <nil> - On v2.0.1:
Variant 1 (traversal): err = no such pluginandVariant 2 (symlink): err = no such plugin
- On v2.0.0:
Evidence
logs/vuln_variant/variant_vuln.txt— test output from v2.0.0 showing traversal and symlink variants succeedlogs/vuln_variant/variant_fixed.txt— test output from v2.0.1 showing both variants blocked byos.OpenRootvuln_variant/patch_analysis.md— detailed diff analysis of the fix
Recommendations / Next Steps
- Harden the Register endpoint: Add
PluginIDvalidation toHostVolume.ValidateUpdateorHostVolume.Registerto reject path traversal characters (..,/,\). This provides defense-in-depth in case the client-sideos.OpenRootis ever bypassed or not available (e.g., on older kernels withoutopenat2). - Normalize/sanitize PluginID early: Apply a basename-only validation (e.g.,
filepath.Base(pluginID) == pluginID && !strings.Contains(pluginID, "..")) at the API boundary before any storage or RPC forwarding. - Audit other filepath.Join usages: Search the codebase for other places where user input is joined with a configured directory path to ensure consistent containment.
Additional Notes
- Idempotency: The reproduction script was run twice consecutively and produced identical results.
- Go version: Tests run with Go 1.24.7 on Linux amd64.
os.OpenRootrequires kernel ≥5.6 foropenat2withRESOLVE_IN_ROOT. - Trust boundary: All tested variants require authenticated access (host-volume-create or host-volume-register ACL). The trust boundary is crossed when the attacker submits an API request that reaches the client RPC.
CVE-2026-7474 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Unknown error
Unknown error
Artifacts and Evidence for CVE-2026-7474
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-7474
Upgrade nomad · go to v2.0.1 or later.
FAQ: CVE-2026-7474
How does the CVE-2026-7474 path-traversal attack work?
Which Nomad versions are affected by CVE-2026-7474, and where is it fixed?
How severe is CVE-2026-7474?
How can I reproduce CVE-2026-7474?
References for CVE-2026-7474
Authoritative sources for CVE-2026-7474 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.