Skip to content

fix(utils): reject tar hardlinks that escape the extraction root - #11266

Merged
mudler merged 3 commits into
mudler:masterfrom
Zelys-DFKH:fix/untar-hardlink-escape
Aug 1, 2026
Merged

fix(utils): reject tar hardlinks that escape the extraction root#11266
mudler merged 3 commits into
mudler:masterfrom
Zelys-DFKH:fix/untar-hardlink-escape

Conversation

@Zelys-DFKH

@Zelys-DFKH Zelys-DFKH commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

ExtractArchive in pkg/utils/untar.go walks archive members before extracting and rejects two things: member paths that resolve outside the destination, and symlinks. Tar hardlink entries are covered by neither. Header.FileInfo() reports a regular file mode for a hardlink, so the symlink test does not match, and Header.Linkname is never inspected. An archive can therefore name a link target outside the extraction directory and the extractor will create it.

This passes Linkname through the same validation already applied to member names, so a hardlink pointing outside the extraction root is rejected with the existing "unsafe path" error. Hardlinks whose target resolves inside the root still extract normally, so ordinary archives are unaffected.

pkg/oci/image.go already handles this on the OCI image path, resolving tar.TypeLink targets against the extraction root before using them. This brings the archive path in line with it.

Notes for Reviewers

Three tests cover this, all in pkg/utils/untar_test.go.

rejects tar hardlinks that point outside the destination writes a tar holding a single tar.TypeLink entry whose Linkname is ../escaped.txt, then asserts extraction fails and nothing lands outside the destination. Without the fix it fails, and the failure output shows the underlying archiver attempting to create the link outside the extraction directory.

rejects tar hardlinks that overwrite a file outside the destination uses a .tar.gz, which is where ExtractArchive binds a Tar config with OverwriteExisting set, and follows the link entry with a regular entry of the same name. Without the fix that pair links to a file above the extraction root and then truncates it through the link, so ExtractArchive returns nil and the outside file comes back holding the archive's contents instead of its own. The plain .tar case never reaches the write.

extracts tar hardlinks that stay inside the destination links to an earlier member of the same archive and expects extraction to succeed, so the new check is not rejecting ordinary archives.

The full pkg/utils package passes on Linux with go1.26.0, 59 specs. gofmt -l and go vet ./pkg/utils/ are clean. golangci-lint would not run because the released binary is built with Go 1.25 while the module targets 1.26.0.

Unrelated to this change and not touched here: the same package's suite hangs on Windows. InTrustedRoot in pkg/utils/path.go loops for path != "/", and filepath.Dir on a Windows volume root returns its input unchanged, so the loop never terminates. I mention it only to explain why I verified on Linux.

Signed commits

  • Yes, I signed my commits.
  • Documentation updated (docs/content/) for user-facing changes, or not applicable

@Zelys-DFKH
Zelys-DFKH force-pushed the fix/untar-hardlink-escape branch from d22d415 to cff3176 Compare July 31, 2026 23:52
ExtractArchive pre-scans archive members and rejects symlinks, but tar
hardlink entries carry a regular file mode and so pass that check.
Header.Linkname was never validated, so an archive could create a link
to a path outside the destination directory.

Validate Linkname with the same path check already applied to member
names. Hardlinks that resolve inside the extraction root still extract,
so ordinary archives are unaffected.

pkg/oci/image.go already resolves tar.TypeLink targets before using
them; this brings the archive extraction path in line with it.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Zelys-DFKH <zelys@dfkhelper.com>
The existing hardlink test names a link target two levels above the
extraction root, so its final assertion checked a path the link never
resolved to and could not fail. Point the target one level up instead,
at the path that assertion already names.

Add two cases. The first uses a .tar.gz, where ExtractArchive binds a
Tar config with OverwriteExisting set, and follows the link entry with a
regular entry of the same name. Before the fix that pair linked to a
file outside the root and then truncated it through the link, which the
plain .tar case does not reach. The second extracts a hardlink whose
target is an earlier member of the same archive, covering the claim that
ordinary archives are unaffected.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Zelys-DFKH <zelys@dfkhelper.com>
@Zelys-DFKH
Zelys-DFKH force-pushed the fix/untar-hardlink-escape branch from cff3176 to 9aa41dc Compare July 31, 2026 23:58

@localai-org-maint-bot localai-org-maint-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the hardlink target validation and its interaction with mholt/archiver: tar hardlinks are resolved as filepath.Join(destination, Linkname), so applying the existing archive-member path validation to Linkname closes the escape while retaining in-root links. The focused archive suite passes locally (8/8), and go vet ./pkg/utils/..., gofmt, and git diff --check are clean. The unrelated full-package failures here were only missing ffmpeg and unavailable DNS. Good to merge. @mudler

@mudler
mudler enabled auto-merge (squash) August 1, 2026 07:25
@mudler
mudler disabled auto-merge August 1, 2026 07:25
@mudler
mudler merged commit 76927cc into mudler:master Aug 1, 2026
1 check passed
@localai-bot localai-bot added the bug Something isn't working label Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants