Skip to content

fix(tar): reject symlink targets that resolve outside destination - #83

Open
johnmccrae wants to merge 1 commit into
mainfrom
jfm/symlink-verify-fix
Open

johnmccrae wants to merge 1 commit into
mainfrom
jfm/symlink-verify-fix

Conversation

@johnmccrae

Copy link
Copy Markdown
Contributor

chef-client -z --recipe-url downloads a tarball and extracts it with mixlib-archive, which followed symlinks in the archive without verifying that the symlink target stayed within the destination directory. A crafted tarball containing a symlink entry pointing outside destination, followed by a second entry whose path traverses that symlink, let an attacker write files anywhere on disk that the extracting process could reach (tar-slip). Since chef-client runs as root, this is an arbitrary file write as root and a path to full node compromise (e.g. dropping a cron.d file, systemd unit, or SSH authorized_keys entry).

  • Tar: before creating a symlink, resolve its target relative to its own directory and skip (with a warning) any symlink whose target resolves outside destination_root. Because every other write is already confined to destination_root, this closes the escape for any chain of symlinks.
  • LibArchive: always set EXTRACT_SECURE_SYMLINKS and EXTRACT_SECURE_NODOTDOT flags on extraction (previously only permission flags were applied), giving the same protection natively in the libarchive-backed extractor used in production.
  • Add regression specs proving the escape is blocked while legitimate in-destination symlinks continue to work.

Signed-off-by: John McCrae john.mccrae@progress.com

Description

Summary

Fixes a path-traversal / arbitrary-file-write vulnerability ("tar-slip") in mixlib-archive's tar extraction. A malicious tarball can contain a symlink entry pointing outside the extraction destination, followed by a second entry whose path travels through that symlink. Previously neither the pure Ruby Tar extractor nor the LibArchive extractor verified that a symlink's target stayed within the destination directory, so the second entry's write landed wherever the attacker's symlink pointed — anywhere on disk.

This is exploitable via chef-client -z --recipe-url <URL>, which downloads and unpacks an untrusted tarball using mixlib-archive. Since chef-client typically runs as root on a managed node, this allows an attacker-controlled tarball to achieve arbitrary file write as root (e.g. /etc/cron.d, a systemd unit, or /root/.ssh/authorized_keys), leading to code execution.

Changes

  • lib/mixlib/archive/tar.rb: When handling a symlink entry, resolve its target relative to the symlink's own directory and reject (log + skip) any target that resolves outside destination_root. Every other entry write is already confined to destination_root, so this closes the escape regardless of symlink chaining.
  • lib/mixlib/archive/lib_archive.rb: Always pass libarchive's EXTRACT_SECURE_SYMLINKS and EXTRACT_SECURE_NODOTDOT extraction flags (previously only permission flags were conditionally set), providing the same protection natively for the libarchive-backed extractor used by default in production.
  • Added regression specs (spec/mixlib/tar_spec.rb) that build a tarball with a symlink escaping the destination plus a payload entry through it, asserting the escape is blocked, and that legitimate in-destination symlinks still extract correctly.
  • Adjusted spec/mixlib/lib_archive_spec.rb to pass the same ./.. ignore list used by the real Mixlib::Archive#extract API, required once EXTRACT_SECURE_NODOTDOT is enforced.

Tests & Coverage

Reproduced the vulnerability against the pre-fix code (confirmed file written outside destination via symlink), then verified the fix closes it. Full suite: bundle exec rake spec (33 examples, 0 failures) and bundle exec rake style (cookstyle, no offenses) both pass.

Risk & Mitigations

Low risk of regression: legitimate symlinks that resolve within the destination directory continue to work (covered by a new spec). Symlinks whose target resolves outside destination are now skipped with a warning log rather than silently followed, which is strictly safer default behavior. No public API signatures changed.

DCO

All commits Signed-off-by.

Related Issue

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Chore (non-breaking change that does not add functionality or fix an issue)

Checklist:

  • I have read the CONTRIBUTING document.
  • I have run the pre-merge tests locally and they pass.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • If Gemfile.lock has changed, I have used --conservative to do it and included the full output in the Description above.
  • All new and existing tests passed.
  • All commits have been signed-off for the Developer Certificate of Origin.

chef-client -z --recipe-url downloads a tarball and extracts it with
mixlib-archive, which followed symlinks in the archive without
verifying that the symlink target stayed within the destination
directory. A crafted tarball containing a symlink entry pointing
outside destination, followed by a second entry whose path traverses
that symlink, let an attacker write files anywhere on disk that the
extracting process could reach (tar-slip). Since chef-client runs as
root, this is an arbitrary file write as root and a path to full node
compromise (e.g. dropping a cron.d file, systemd unit, or SSH
authorized_keys entry).

- Tar: before creating a symlink, resolve its target relative to its
  own directory and skip (with a warning) any symlink whose target
  resolves outside destination_root. Because every other write is
  already confined to destination_root, this closes the escape for
  any chain of symlinks.
- LibArchive: always set EXTRACT_SECURE_SYMLINKS and
  EXTRACT_SECURE_NODOTDOT flags on extraction (previously only
  permission flags were applied), giving the same protection natively
  in the libarchive-backed extractor used in production.
- Add regression specs proving the escape is blocked while legitimate
  in-destination symlinks continue to work.

Signed-off-by: John McCrae john.mccrae@progress.com

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: John McCrae <john.mccrae@progress.com>
@johnmccrae
johnmccrae requested a review from jaymzh as a code owner September 22, 2026 19:26
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.

1 participant