Share
## https://sploitus.com/exploit?id=A30959A2-8AA3-5794-BD6E-FBE6F5AC3A33
# CVE-2026-5061
Consul Template validated where a symlink pointed during template evaluation, but its later dependency fetch read the original path. Retargeting the link between those operations turned an in-sandbox file reference into an out-of-sandbox file disclosure.

## Intro

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](https://discuss.hashicorp.com/t/hcsec-2026-12-consul-template-vulnerable-to-sandbox-path-bypass-in-file-helper-through-symlink-attack/77414)  
**IBM bulletin:** [CVE-2026-5061 security bulletin](https://www.ibm.com/support/pages/security-bulletin-consul-template-vulnerable-sandbox-path-bypass-file-helper-symlink-attack)  
**CVE:** [CVE-2026-5061](https://vulners.com/cve/CVE-2026-5061)  
**Fixed in:** `0.42.0`



---

## Attack Chain

`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`

---

## What Consul Template Does

**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:

- paths passed to `file` must fall inside the configured sandbox
- relative paths must not escape the sandbox
- linked targets must not turn an allowed path into an external file read

That 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.

---

## Why This Surface Was Worth Looking At

Filesystem sandboxes often fail at the gap between path validation and file access.

The common pattern is:

- validate a path
- release control back to the filesystem
- use the path again later
- assume it still identifies the same object

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.

---

## Root Cause

The root cause was a time-of-check to time-of-use mismatch between:

- sandbox validation in `template/funcs.go`
- the later dependency read in `dependency/file.go`

At the tested revision, `fileFunc()` did this:

```go
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
    return "", err
}

d, err := dep.NewFileQuery(s)
```

The validation helper correctly resolved symlinks before checking containment:

```go
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))

rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
    return fmt.Errorf("'%s' is outside of sandbox", path)
}
```

So the sandbox check itself understood the resolved target.

But that resolved target was discarded.

`NewFileQuery()` stored the original string instead:

```go
return &FileQuery{
    stopCh: make(chan struct{}, 1),
    path:   s,
}, nil
```

Then the later dependency fetch read that path again:

```go
data, err := os.ReadFile(d.path)
```

That is the whole vulnerability.

The code checked one filesystem resolution, then later used another.

### Why this is exploitable

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:

- the link resolves to a safe file during `pathInSandbox()`
- validation succeeds
- `FileQuery` records the raw link path
- the link is replaced or retargeted to an external file
- `os.ReadFile(d.path)` resolves the link again
- the outside file is read
- the fetched value is cached in the template brain
- the link is restored before the next template evaluation
- validation succeeds again
- cached external content is returned and rendered

That last step is important.

Restoring the safe link did not remove the already-fetched secret from the dependency cache.

---

## What Makes This a Security Issue, Not Just a Filesystem Race

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:

- this target is inside the sandbox
- therefore it is safe to register and fetch

But the later fetch was not tied to the target that justified that decision.

That is what turned ordinary filesystem mutability into a vulnerability.

---

## Proof of Concept

I built a standalone reproducer around the exact evaluation and dependency-fetch sequence.

The controlled setup used:

- a configured sandbox directory
- a safe file inside that sandbox
- a secret file outside the sandbox
- a watched symlink path inside the sandbox
- deterministic link swaps around the dependency fetch

The reproduction flow was:

1. Point the watched symlink at the safe in-sandbox file.
2. Evaluate the `file` helper so sandbox validation succeeds and the raw path is registered as a dependency.
3. Retarget the watched symlink to the outside secret before dependency fetch.
4. Run the dependency fetch and allow `os.ReadFile(d.path)` to read through the redirected link.
5. Store the fetched value in the template cache.
6. Restore the symlink to the safe in-sandbox file.
7. Evaluate the template again.
8. Confirm sandbox validation still succeeds.
9. Confirm the rendered output contains the previously fetched outside secret.

The observed behavior was:

- initial sandbox validation passed
- the dependency retained the original link path
- the fetch read the out-of-sandbox file after the link changed
- the safe target was restored before the next render
- the next sandbox check passed
- the rendered output matched the external secret exactly by SHA-256

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.

---

## Why the PoC Was Chosen This Way

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:

- safe at validation time
- unsafe at fetch time
- safe again at render time
- external content still consumed from cache

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.

---

## Exploitation Requirements and Scope

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:

- have permission to read the external target
- evaluate a template using the attacker-influenced path
- expose the rendered result somewhere the attacker can recover or influence its use

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:

- bypass of the documented `sandbox_path` restriction
- out-of-sandbox local file disclosure
- possible exposure of secrets readable by the Consul Template process

The risk increases when the process runs with access to sensitive credentials, service configuration, tokens, or private keys.

---

## Severity and Classification

HashiCorp classified the issue as:

- **CWE-59**: Improper Link Resolution Before File Access
- **CVSS 3.1:**

```text
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
```

The published base score is:

```text
4.7 / Medium
```

That scoring reflects the real constraints:

- local attack vector
- low privileges required to influence the path
- high attack complexity because the link must change during the relevant lifecycle window
- no victim interaction required
- high confidentiality impact if a sensitive process-readable file is fetched

That is a reasonable classification.

The issue is narrow in attacker prerequisites, but the sandbox bypass and resulting file disclosure are both concrete.

---

## Affected Versions

The HashiCorp bulletin lists:

```text
Affected: consul-template up to 0.41.4
Fixed:    consul-template 0.42.0
```

The fix shipped in the `0.42.0` release on **April 15, 2026**.

---

## Fix Analysis

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()`:

```go
resolvedPath, err := resolveSandboxedPath(
    sandboxPath,
    strings.TrimSpace(s),
)
if err != nil {
    return "", err
}

d, err := dep.NewFileQuery(resolvedPath)
```

The security property changed from:

- validate resolved target
- discard resolved target
- fetch through raw path later

to:

- validate resolved target
- retain resolved target
- fetch through that validated path

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:

- safe symlink during validation
- external symlink during fetch
- safe symlink restored before the next call
- cached external secret must not be returned

That is the kind of remediation you want for this bug:

- fix the broken validation/use binding
- preserve the sandbox behavior
- add a regression test for the full race lifecycle

---

## Disclosure

I reported this issue privately to HashiCorp Security on **March 20, 2026**.

The report included:

- source-level root cause analysis
- the validation-to-fetch TOCTOU sequence
- a standalone deterministic reproducer
- runtime output
- SHA-256 evidence showing the rendered data matched the external secret
- affected revision details

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)**

---

## What This Bug Actually Teaches

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:

- sandbox checks
- upload destination checks
- archive extraction
- local secret reads
- temporary-file handling
- privilege-separated file operations

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.

---

## Key Points

- `sandbox_path` was an explicit local-file security boundary
- `pathInSandbox()` resolved and validated the symlink target correctly
- the resolved target was discarded after validation
- `FileQuery` retained the original attacker-influenced path
- dependency fetch later resolved that path again through `os.ReadFile`
- restoring the safe link did not remove the already-cached external content
- the PoC proved byte-exact out-of-sandbox disclosure with SHA-256
- version `0.42.0` fixed the bug by binding the dependency to the resolved, validated path

---

## Final Words

This 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**.