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
IBM bulletin: CVE-2026-14361 security bulletin
CVE: CVE-2026-14361
Fixed in: 0.42.1
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
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:
That makes this more than an ordinary output helper.
The content crossing this boundary may include:
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.
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:
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.
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:
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.
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:
Normal write mode used:
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 fileos.OpenFile(path, ...) follows linked path components during append modeThe same path-based assumption continued after the write.
Ownership and permissions were applied using the path again:
That meant metadata operations were also tied to a mutable path name instead of the already-open file descriptor.
Because the attacker can prepare the redirection before writeToFile runs.
No probabilistic race is required for the basic attack.
The sequence is straightforward:
writeToFile opens it directlyThat is the whole vulnerability.
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:
Following attacker-positioned filesystem redirection in that context creates a real privilege and trust-boundary problem.
I built a standalone reproducer around the exact writeToFile behavior at the tested commit.
The controlled setup used:
writeToFileThe reproduction flow was:
The observed behavior was:
writeToFile followed the redirectionThat established both parts of the claim:
A final-component symlink would already demonstrate unsafe link following.
But a linked parent component proves a stronger operational point:
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.
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:
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.
HashiCorp classified the issue as:
The published base score is:
That vector reflects:
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.
The HashiCorp bulletin lists:
The fix shipped in the 0.42.1 release on July 8, 2026.
The 0.42.1 patch hardened the write path at several layers.
The patched helper uses os.Lstat() to inspect:
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.
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.
The patch replaced path-based ownership and mode operations with descriptor-based operations:
That binds metadata changes to the file that was actually opened instead of resolving the path again later.
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.
The patch added focused tests for:
The public patch discussion documents an important limitation clearly.
The new validation checks:
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:
That distinction is worth preserving in a technical writeup.
I reported this issue privately to HashiCorp Security on March 21, 2026 as a second independent Consul Template finding.
The report included:
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)
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:
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.
writeToFile is documented for sensitive material such as certificates and private keysos.Create or os.OpenFileChown and Chmod introduced additional mutable-path trust0.42.1 added link checks, O_NOFOLLOW where supported, descriptor-based metadata operations, and regression testsThis 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.