I found this issue while reviewing HashiCorp Consul Template, with a very specific security question in mind:
If sandbox_path validates a symlink during template evaluation, does the later file read stay bound to that same validated target?
In this case, the answer was no.
The file template helper resolved the path and enforced the configured sandbox during template evaluation. But after that check, it created a file dependency using the original raw path.
That dependency was fetched later.
If an attacker retargeted the symlink in the gap between validation and dependency fetch, Consul Template could read an out-of-sandbox file. If the link was restored before the next render, sandbox validation passed again and the cached external content was still rendered.
That issue became CVE-2026-5061.
HashiCorp bulletin: HCSEC-2026-12
IBM bulletin: CVE-2026-5061 security bulletin
CVE: CVE-2026-5061
Fixed in: 0.42.0
attacker-controlled in-sandbox symlink -> sandbox validation resolves to safe target -> dependency stores original raw path -> attacker retargets link outside sandbox -> dependency fetch reads external file -> attacker restores safe link -> next validation passes -> cached external content is rendered
Consul Template is a template rendering tool for Consul and Vault data.
It can run continuously, watch dependencies for changes, and render data into files or environment variables for applications to consume.
The file template helper reads a local file and inserts its contents into rendered output.
Because local file reads can expose process-readable secrets, Consul Template provides sandbox_path as a boundary for this helper.
The documented security property is straightforward:
file must fall inside the configured sandboxThat makes sandbox_path a real security boundary.
The important question was not whether the path looked like it was under the sandbox.
The real question was:
Does the file that gets read remain the same file that passed sandbox validation?
In vulnerable versions, it did not.
Filesystem sandboxes often fail at the gap between path validation and file access.
The common pattern is:
That assumption is unsafe when an attacker can modify a symlink or equivalent filesystem redirection between the two operations.
Consul Template made this surface especially interesting because template evaluation and dependency fetching were separate stages.
That separation created the right security question:
Is the dependency fetch bound to the validated target, or does it resolve the attacker-controlled path again?
That was the boundary I focused on.
The root cause was a time-of-check to time-of-use mismatch between:
template/funcs.godependency/file.goAt the tested revision, fileFunc() did this:
The validation helper correctly resolved symlinks before checking containment:
So the sandbox check itself understood the resolved target.
But that resolved target was discarded.
NewFileQuery() stored the original string instead:
Then the later dependency fetch read that path again:
That is the whole vulnerability.
The code checked one filesystem resolution, then later used another.
Because a symlink is mutable.
The attacker does not need to make an out-of-sandbox target pass pathInSandbox() directly.
They only need to change what the already-approved raw path resolves to after validation but before Fetch() performs the read.
The sequence is:
pathInSandbox()FileQuery records the raw link pathos.ReadFile(d.path) resolves the link againThat last step is important.
Restoring the safe link did not remove the already-fetched secret from the dependency cache.
The important distinction is bypass of an explicit security control.
This was not merely:
"the file changed while Consul Template was watching it"
Watching files for changes is expected behavior.
The real issue was:
a path that passed the documented sandbox restriction could later be used to read a file outside that sandbox
That is a direct trust-boundary failure.
The application had already made a security decision:
But the later fetch was not tied to the target that justified that decision.
That is what turned ordinary filesystem mutability into a vulnerability.
I built a standalone reproducer around the exact evaluation and dependency-fetch sequence.
The controlled setup used:
The reproduction flow was:
file helper so sandbox validation succeeds and the raw path is registered as a dependency.os.ReadFile(d.path) to read through the redirected link.The observed behavior was:
That hash comparison mattered.
It proved that the final rendered value was not stale safe content, a filename artifact, or an error-path side effect.
It was the byte-exact content of the out-of-sandbox file.
The strongest proof for a TOCTOU issue must control the timeline.
Simply showing that a symlink can point outside the sandbox would be weaker because pathInSandbox() already rejected an external target when it observed one.
The security claim depended on proving all of these states in order:
That is why the reproducer separated template validation, dependency fetch, link restoration, and the next render explicitly.
The PoC did not rely on random timing or repeated guessing.
It drove the vulnerable lifecycle deterministically.
That made the root cause and the impact much easier to defend.
This issue requires local filesystem influence.
An attacker needs enough access to create, replace, or retarget the relevant symlink or equivalent linked path during the vulnerable window.
The Consul Template process must also:
Those requirements matter.
This was not an unauthenticated remote arbitrary file read from any default deployment.
But within the affected local trust model, the impact was meaningful:
sandbox_path restrictionThe risk increases when the process runs with access to sensitive credentials, service configuration, tokens, or private keys.
HashiCorp classified the issue as:
The published base score is:
That scoring reflects the real constraints:
That is a reasonable classification.
The issue is narrow in attacker prerequisites, but the sandbox bypass and resulting file disclosure are both concrete.
The HashiCorp bulletin lists:
The fix shipped in the 0.42.0 release on April 15, 2026.
The fix was small and directly addressed the broken binding.
Instead of validating the resolved target and then building the dependency from the raw input, the patched code returns the resolved path and gives that path to NewFileQuery():
The security property changed from:
to:
That closes the reported symlink-retargeting path because changing the original link no longer changes the path stored by the dependency.
The patch also added a focused regression test covering the exact sequence:
That is the kind of remediation you want for this bug:
I reported this issue privately to HashiCorp Security on March 20, 2026.
The report included:
HashiCorp fixed the issue in 0.42.0 and published HCSEC-2026-12 on May 12, 2026.
IBM published a corresponding security bulletin for the same CVE.
Both official bulletins acknowledged the report as:
Mohamed Abdelaal (0xmrma)
The key lesson is simple:
validating a path is not enough if the later file operation can resolve that path to a different object
That rule applies far beyond Consul Template.
It matters anywhere code does:
The deeper security property is not:
"the path string looked safe once"
It is:
the object used by the sensitive operation must be the object that passed validation
In this case, Consul Template validated one resolution and fetched another.
That gap was enough.
sandbox_path was an explicit local-file security boundarypathInSandbox() resolved and validated the symlink target correctlyFileQuery retained the original attacker-influenced pathos.ReadFile0.42.0 fixed the bug by binding the dependency to the resolved, validated pathThis vulnerability was not about bypassing a string-prefix check.
It was about time and identity.
Consul Template checked where the link pointed during template evaluation. The dependency fetch later asked the filesystem the same question again. An attacker could change the answer between those two moments.
That is why this became CVE-2026-5061.
Fixed in consul-template 0.42.0.