## https://sploitus.com/exploit?id=E869A2E5-6F70-5494-BE5A-28B8BEE8A469
# CVE-2026-14361
Consul Template's writeToFile helper opened an operator-supplied destination directly and followed linked path components, allowing rendered output to escape the intended directory and overwrite a preexisting file.
## Intro
I found this issue while reviewing **HashiCorp Consul Template**, with a direct filesystem security question in mind:
**If `writeToFile` receives a path that appears to be inside the intended directory, does it verify where the filesystem will actually write the data?**
In this case, the answer was no.
The `writeToFile` template helper opened the final user-supplied path directly through `os.Create()` or `os.OpenFile()`.
Those operations followed symbolic links, directory junctions, and equivalent filesystem redirections already present in the destination path.
That meant the path string could remain under the operator's intended root while the actual write landed somewhere else.
In my controlled proof of concept, a linked parent directory redirected rendered output outside the intended tree and caused a preexisting target file to be clobbered.
That issue became **CVE-2026-14361**.
**HashiCorp bulletin:** [HCSEC-2026-20](https://discuss.hashicorp.com/t/hcsec-2026-20-consul-template-vulnerable-to-path-redirections-in-writetofile/77559)
**IBM bulletin:** [CVE-2026-14361 security bulletin](https://www.ibm.com/support/pages/security-bulletin-consul-template-vulnerable-path-redirection-writetofile-through-symlink-attack)
**CVE:** [CVE-2026-14361](https://vulners.com/cve/CVE-2026-14361)
**Fixed in:** `0.42.1`
---
## Attack Chain
`operator-supplied destination appears inside intended root -> attacker pre-positions linked parent or final path component -> writeToFile opens the path directly -> filesystem resolves the write outside the intended directory -> rendered secret is redirected -> preexisting target may be overwritten`
---
## What writeToFile Does
Consul Template renders data from sources such as Consul and Vault.
The `writeToFile` helper lets a template write selected content to a separate local file while applying a requested owner, group, and permission mode.
HashiCorp's documentation specifically demonstrates the helper with PKI material:
```text
private key -> writeToFile /my/path/to/cert.key
certificate authority -> writeToFile /my/path/to/cert.pem
certificate -> append to /my/path/to/cert.pem
```
That makes this more than an ordinary output helper.
The content crossing this boundary may include:
- private keys
- certificates
- secrets fetched from Vault
- configuration values
- service credentials
The important question was not whether `writeToFile` could create a requested filename.
The real question was:
**Does the process write to the operator's intended filesystem location, or merely to whatever object the path resolves to at open time?**
In vulnerable versions, it trusted the latter.
---
## Why This Surface Was Worth Looking At
Write helpers are high-value security surfaces because they cross from application data into filesystem mutation.
The interesting failures are often not classic `../` traversal.
They are resolution failures:
- the string looks safe
- the directory tree looks operator-controlled
- a linked component changes the real destination
- the open call follows that redirection automatically
That is especially important when the process runs with more filesystem privileges than the attacker.
A low-privileged local attacker may not be able to overwrite a sensitive file directly.
But if they can influence a path component under an intended write directory, a more privileged Consul Template process may perform the write for them.
That was the boundary I focused on.
---
## The Boundary I Focused On
I did not approach this as a generic path traversal review.
The supplied path did not need `..` segments.
It could remain lexically inside the intended root the entire time.
The stronger question was:
> **Are linked destination components rejected before sensitive content is written?**
That question matters for both:
- a linked final filename
- a linked directory component that redirects the whole remaining path
The second case is especially useful because logs and configuration still show an innocent-looking path under the expected directory.
The filesystem resolves it somewhere else.
---
## Root Cause
The root cause was direct path-based file creation without link-aware validation.
At the tested revision, `writeToFile()` selected one of two open paths.
Append mode used:
```go
f, err = os.OpenFile(
path,
os.O_APPEND|os.O_WRONLY|os.O_CREATE,
perm,
)
```
Normal write mode used:
```go
dirPath := filepath.Dir(path)
if _, err := os.Stat(dirPath); err != nil {
err := os.MkdirAll(dirPath, os.ModePerm)
if err != nil {
return "", err
}
}
f, err = os.Create(path)
```
Neither path rejected linked destination components before the file was opened.
That matters because:
- `os.Create(path)` follows existing filesystem redirections and truncates the resolved file
- `os.OpenFile(path, ...)` follows linked path components during append mode
- a linked directory changes where the final filename is resolved
- a linked final component can redirect the open to a different existing file
The same path-based assumption continued after the write.
Ownership and permissions were applied using the path again:
```go
err = os.Chown(path, uid, gid)
err = os.Chmod(path, perm)
```
That meant metadata operations were also tied to a mutable path name instead of the already-open file descriptor.
### Why this is exploitable
Because the attacker can prepare the redirection before `writeToFile` runs.
No probabilistic race is required for the basic attack.
The sequence is straightforward:
- operator configures a destination under an expected root
- attacker gains enough local access to create or replace a linked component beneath that write location
- the path string still appears to stay under the intended root
- `writeToFile` opens it directly
- the operating system follows the link or junction
- rendered output lands at the resolved destination
- if that destination already exists, normal create mode truncates and overwrites it
That is the whole vulnerability.
---
## What Makes This a Security Issue, Not Just Normal Symlink Behavior
It is true that operating systems normally follow symlinks during path-based file opens.
That does not make this safe application behavior.
The security question is not:
> "Did Go behave as documented?"
The real question is:
> **Did a helper writing sensitive rendered data verify that the resolved destination matched the operator's intended location?**
In vulnerable versions, it did not.
That distinction matters because Consul Template may run:
- as a long-lived service
- with access to Vault-derived secrets
- under an elevated account
- with write access that the local attacker does not have directly
Following attacker-positioned filesystem redirection in that context creates a real privilege and trust-boundary problem.
---
## Proof of Concept
I built a standalone reproducer around the exact `writeToFile` behavior at the tested commit.
The controlled setup used:
- an intended output root
- an external directory outside that root
- a linked parent component beneath the intended root
- a preexisting target file in the redirected directory
- controlled secret content passed to `writeToFile`
The reproduction flow was:
1. Create the intended output root.
2. Create a separate external target directory.
3. Place a preexisting target file in the external directory.
4. Create a linked parent directory under the intended root that resolves to the external directory.
5. Build the final destination path using the linked parent while keeping the path string under the intended root.
6. Invoke the vulnerable write path with controlled secret content.
7. Resolve and inspect the actual destination.
8. Compare the final file bytes and SHA-256 with the secret input.
The observed behavior was:
- the intended destination string remained under the intended root
- the linked parent redirected the actual write outside that root
- `writeToFile` followed the redirection
- the preexisting external target was clobbered
- the final external file matched the secret input exactly by SHA-256
That established both parts of the claim:
- output could escape the intended directory tree
- an existing file at the resolved destination could be overwritten
---
## Why the PoC Was Chosen This Way
A final-component symlink would already demonstrate unsafe link following.
But a linked parent component proves a stronger operational point:
- the configured filename can look completely normal
- the path can remain lexically under the intended root
- the redirection can happen higher in the path
- the final write can still land elsewhere
The preexisting target also mattered.
Without it, the PoC would show only unexpected file creation.
By starting with an existing file and verifying its final hash, the reproduction proved the overwrite behavior directly.
That made the result more concrete than a source-only claim.
---
## Exploitation Requirements and Scope
This issue requires local filesystem influence.
An attacker needs enough access to create or modify a symlink, directory junction, or equivalent redirection in or beneath the intended write location.
The Consul Template process must then write through that path.
The impact depends heavily on process privileges and rendered content.
The practical outcomes include:
- redirecting template output outside the operator's intended directory
- placing sensitive rendered data in an attacker-readable location
- overwriting a preexisting file at the resolved destination
- increasing impact when the process runs with elevated privileges
- increasing confidentiality risk when the content contains keys, certificates, or secrets
This was not a remote unauthenticated arbitrary write from a default installation.
But it was a clear local trust-boundary failure with meaningful impact in privileged or shared-filesystem deployments.
---
## 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 vector reflects:
- local attacker access
- low privileges needed to influence the path
- high attack complexity
- no user interaction
- potentially high confidentiality impact when sensitive rendered output is redirected
The official bulletins also explicitly document that the redirected write can overwrite a preexisting file.
The score is moderate because exploitation depends on local path-control conditions, not because the filesystem effect is theoretical.
---
## Affected Versions
The HashiCorp bulletin lists:
```text
Affected: consul-template up to and including 0.42.0
Fixed: consul-template 0.42.1
```
The fix shipped in the `0.42.1` release on **July 8, 2026**.
---
## Fix Analysis
The `0.42.1` patch hardened the write path at several layers.
### 1. Reject linked destination components
The patched helper uses `os.Lstat()` to inspect:
- the immediate parent directory
- the final destination component
and rejects those components when they are links.
That closes the direct parent-redirection and final-file-symlink cases covered by the report and regression tests.
### 2. Use O_NOFOLLOW for the final open where supported
On supported Unix platforms, the destination is opened with `O_NOFOLLOW`.
That makes the open itself fail if the final component becomes a symlink between the pre-check and the open.
This is important because a pre-check alone can create another TOCTOU window.
The platform-specific implementation is a no-op on Windows, where `O_NOFOLLOW` is not available through the same mechanism.
### 3. Apply metadata through the open descriptor
The patch replaced path-based ownership and mode operations with descriptor-based operations:
```go
f.Chown(uid, gid)
f.Chmod(perm)
```
That binds metadata changes to the file that was actually opened instead of resolving the path again later.
### 4. Fail closed on unexpected stat errors
The helper now creates the parent directory only when the stat failure is genuinely `os.IsNotExist`.
Other errors, such as permission failures, are returned instead of continuing into the write path.
### Regression coverage
The patch added focused tests for:
- final-component symlink rejection
- linked parent-directory rejection
- normal path success
- append-mode final symlink rejection
- byte verification that the sensitive target remains unchanged
---
## An Important Fix Boundary
The public patch discussion documents an important limitation clearly.
The new validation checks:
- the immediate parent directory
- the final path component
It does not walk and reject every higher ancestor component.
The maintainers chose that boundary because `writeToFile` has no configured sandbox root to anchor a complete containment check, and common operating systems may include legitimate managed links in path prefixes, such as `/var -> /private/var` on macOS.
`O_NOFOLLOW` also protects the final component, not every ancestor directory.
That does not change the status of the reported vulnerability or the official fixed version.
It does clarify the exact security property delivered by the patch:
- the reported immediate-parent and final-component redirection paths are rejected
- metadata operations are bound to the opened descriptor
- the helper does not claim to provide a general filesystem sandbox for every ancestor path
That distinction is worth preserving in a technical writeup.
---
## Disclosure
I reported this issue privately to HashiCorp Security on **March 21, 2026** as a second independent Consul Template finding.
The report included:
- source-level root cause analysis
- a standalone proof of concept
- runtime output
- intended and resolved path evidence
- before/after file behavior
- SHA-256 confirmation that the redirected target matched the secret input
- affected revision details
The original follow-up email was not present in the security team's report queue, likely because it was caught by a mailing-list spam filter.
After I re-forwarded the complete report, HashiCorp connected with the engineering team and investigated it separately from the first Consul Template issue.
HashiCorp fixed the vulnerability in `0.42.1` and published **HCSEC-2026-20** on **July 8, 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:
> a path string is not the same thing as the filesystem object it names
That distinction matters whenever privileged code writes attacker-influenced paths.
Checking that a string begins with an intended directory is not enough.
Even a clean path without traversal sequences can resolve somewhere else through:
- symbolic links
- directory junctions
- mount redirections
- mutable path components
The sensitive operation must be tied to a destination whose identity has been validated at the right boundary.
This issue also reinforces a broader rule:
> when you already have an open file descriptor, apply security-sensitive operations through that descriptor instead of resolving the path again
That is exactly why the descriptor-based `Chown` and `Chmod` changes matter.
---
## Key Points
- `writeToFile` is documented for sensitive material such as certificates and private keys
- vulnerable versions opened the destination directly with `os.Create` or `os.OpenFile`
- linked parent and final components could redirect the actual write
- the configured path could remain under the intended root while the resolved target was outside it
- normal create mode could truncate and overwrite a preexisting target
- path-based `Chown` and `Chmod` introduced additional mutable-path trust
- the PoC proved redirection and overwrite with byte-exact SHA-256 comparison
- version `0.42.1` added link checks, `O_NOFOLLOW` where supported, descriptor-based metadata operations, and regression tests
---
## Final Words
This vulnerability was not about `../` traversal.
The path string looked correct.
The filesystem destination was not.
Consul Template accepted the operator's intended path, followed an attacker-positioned redirection, and wrote the rendered content to a different file.
That is why this became **CVE-2026-14361**.
Fixed in **consul-template 0.42.1**.