Skip to content

Prevent path redirection in writeToFile via symlinked path components - #2156

Merged
Surabhi-1605 merged 4 commits into
mainfrom
fix-writetofile-symlink
Jul 8, 2026
Merged

Surabhi-1605 merged 4 commits into
mainfrom
fix-writetofile-symlink

Conversation

@Surabhi-1605

Copy link
Copy Markdown
Collaborator

Summary

The writeToFile template helper opened the final user-supplied path directly with os.Create / os.OpenFile, which follow symlinks in both intermediate directory components and the final filename. This allowed template output to be redirected outside the intended directory, or to overwrite an existing file via a pre-positioned symlink — a particular concern given writeToFile is documented for writing certificate and private-key material.

Unlike the main renderer's AtomicWrite (temp-file + rename, which replaces the directory entry rather than following it), writeToFile wrote directly to the named path and lost that protection.

Changes

  • Validate the write path before opening it:
    • Reject a symlinked parent directory component (os.Lstat), which would redirect the whole write into an attacker-controlled directory.
    • Reject a symlinked final filename component (os.Lstat), which would clobber an unintended target.
  • Open the destination with O_NOFOLLOW (via a per-platform constant; no-op on Windows) so the open itself fails if the final component is a symlink, closing the TOCTOU window between the pre-check and the open.
  • Apply ownership and permissions through the open file descriptor (f.Chown / f.Chmod) instead of the path, so metadata always targets the file that was opened rather than a path that could be swapped afterward.
  • Fail closed: only create the parent directory when it genuinely does not exist (os.IsNotExist), and return immediately if the destination cannot be stat'd for any reason other than non-existence (e.g. permission denied), instead of proceeding to open.

os.Lstat is used rather than filepath.EvalSymlinks containment (as used by the sandboxed file helper) because writeToFile has no configured sandbox boundary, and an EvalSymlinks-based ancestor check produces false positives on OS-managed symlinks (e.g. macOS /var -> /private/var).

Known limitation

Only the immediate parent directory is checked, not grandparent/ancestor components. This is deliberate: writeToFile has no sandbox to anchor a full ancestor walk, and walking every ancestor breaks compatibility on systems with OS-managed symlinks in common path prefixes. O_NOFOLLOW only guards the final path component; a residual TOCTOU on ancestor directories remains, consistent with this limitation.

Tests

Adds Test_writeToFile_symlink_rejection:

  • rejects_final_component_symlink — errors, sensitive file byte-verified unchanged
  • rejects_directory_component_symlink — errors, redirect target byte-verified unchanged
  • allows_normal_path — succeeds
  • append_rejects_final_component_symlink — errors in append mode, sensitive file unchanged

Sub-tests skip gracefully where os.Symlink is unsupported and skip entirely as root. All pre-existing Test_writeToFile cases continue to pass.

@Surabhi-1605
Surabhi-1605 requested review from a team as code owners July 8, 2026 10:02
writeToFile opened the final user-supplied path directly with os.Create/os.OpenFile, following symlinks in both the parent directory and the final filename. This allowed template output to be redirected outside the intended directory or to clobber an existing file. Reject symlinked parent and final path components using os.Lstat before opening, and route all write, chown, and chmod operations through the validated path.
Open the destination with O_NOFOLLOW so the open fails if the final path
component is a symlink, closing the TOCTOU window between the pre-check and
the open. Apply ownership and permissions via the open file descriptor
(f.Chown/f.Chmod) instead of the path so metadata always targets the file we
opened. O_NOFOLLOW is provided per-platform (no-op on Windows).
Gate MkdirAll on os.IsNotExist so writeToFile only creates the parent
directory when it is genuinely missing, and surfaces other stat errors
(e.g. permission denied) directly instead of masking them with a MkdirAll
failure.
Return early if os.Lstat on the destination fails for any reason other than
the file not existing (e.g. permission denied), rather than proceeding to
open. This keeps the final-component check consistent with the parent
directory check above and avoids continuing when we cannot verify the
target is safe.
@Surabhi-1605
Surabhi-1605 force-pushed the fix-writetofile-symlink branch from acce9d9 to bea967e Compare July 8, 2026 10:21

@panman90 panman90 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread CHANGELOG.md Resolved
@Surabhi-1605
Surabhi-1605 merged commit f39114c into main Jul 8, 2026
55 of 58 checks passed
@Surabhi-1605
Surabhi-1605 deleted the fix-writetofile-symlink branch July 8, 2026 10:40
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
consul-template 0.42.1

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>0.42.1 (July 08, 2026)

IMPROVEMENTS:

cve: fix path redirection and unintended file overwrite in writeToFile helper through symlink attack hashicorp/consul-template#2156
update: go version to 1.26.4 hashicorp/consul-template#2155
update: Bump github.com/hashicorp/consul/api v1.34.0 to 1.34.4 https://github.com/hashicorp/consul-template/pull/2155</pre>
  <p>View the full release notes at <a href="https://github.com/hashicorp/consul-template/releases/tag/v0.42.1">https://github.com/hashicorp/consul-template/releases/tag/v0.42.1</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!13782
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

⌂ 换网址