Skip to content

Unpack, UnpackLayer: treat tar entry names as POSIX paths - #63

Draft
thaJeztah wants to merge 4 commits into
moby:mainfrom
thaJeztah:unpack_posix
Draft

Unpack, UnpackLayer: treat tar entry names as POSIX paths#63
thaJeztah wants to merge 4 commits into
moby:mainfrom
thaJeztah:unpack_posix

Conversation

@thaJeztah

@thaJeztah thaJeztah commented Jul 21, 2026

Copy link
Copy Markdown
Member

Unpack: treat tar entry names as POSIX paths

Normalize tar entry names with path.Clean instead of filepath.Clean.

Tar header paths always use POSIX ('/') separators. Preserve their POSIX
form while operating on archive metadata, deferring conversion to native
paths until the filesystem boundary.

This fixes a bug where ExcludePatterns could fail to match valid archive
entries on Windows because filepath.Clean converted tar paths to native
path separators before exclusion matching.

UnpackLayer: treat tar entry names as POSIX paths

Normalize tar entry names with path.Clean instead of filepath.Clean.

Tar header paths always use POSIX ('/') separators. Preserve their POSIX
form while operating on archive metadata, deferring conversion to native
paths until the filesystem boundary.

UnpackLayer: localize AUFS hardlink basenames

Use filepath.Localize when converting AUFS hardlink basenames from tar
header paths to native filesystem paths. This validates that the
basename is a single native path component before it is passed to
filepath.Join(), avoiding reinterpretation of Windows path separators.

UnpackLayer: tighten AUFS whiteout path matching

Only treat hardlinks targeting entries inside /.wh..wh.plnk as AUFS
metadata by requiring a path-component boundary after the directory
name. This avoids matching similarly prefixed paths while preserving
existing handling of AUFS metadata entries and opaque whiteouts.

Copilot AI review requested due to automatic review settings July 21, 2026 23:13
@codecov-commenter

codecov-commenter commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.46154% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.09%. Comparing base (e04f49b) to head (c5d1233).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
diff.go 21.05% 12 Missing and 3 partials ⚠️
archive.go 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #63      +/-   ##
==========================================
- Coverage   65.75%   65.09%   -0.67%     
==========================================
  Files          42       42              
  Lines        2038     2051      +13     
==========================================
- Hits         1340     1335       -5     
- Misses        528      541      +13     
- Partials      170      175       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread archive.go Fixed
Comment thread archive.go

// #nosec G305 -- The joined path is checked for path traversal.
dstPath := filepath.Join(dest, hdr.Name)
dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
Comment thread diff.go Fixed
@thaJeztah
thaJeztah marked this pull request as ready for review July 21, 2026 23:19
Comment thread diff.go
// Strip a leading "/" so absolute entries stay root-relative, and
// normalize the POSIX tar path. Skip entries referring to the extraction
// root and reject paths that escape it.
name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
@thaJeztah

Copy link
Copy Markdown
Member Author

CodeQL issues will be fixed in follow-up(s)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates archive extraction to treat tar header entry names as POSIX paths (using path.Clean), then converts to native paths only at the filesystem boundary, improving cross-platform correctness (notably Windows) and strengthening path traversal protection by rejecting non-local entries via filepath.IsLocal.

Changes:

  • Normalize tar header names with path.Clean (POSIX semantics) in both Unpack and UnpackLayer.
  • Reject non-local (potentially escaping) entry names using filepath.IsLocal after normalization.
  • Convert POSIX paths to native paths only when constructing filesystem paths (filepath.FromSlash), and use path.Base / path.Clean for tar-metadata operations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
archive.go Updates Unpack to normalize tar entry names as POSIX paths, apply IsLocal validation, and defer native-path conversion until joining paths for extraction.
diff.go Updates UnpackLayer to normalize and validate POSIX tar entry names, and to use POSIX path helpers when operating on tar metadata (e.g., AUFS hardlink handling).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread diff.go Outdated
Comment thread archive.go Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

diff.go:121

  • Path traversal guard misses the case where filepath.Rel returns exactly ".." (e.g., a tar entry name of ".." or "../"). In that case the current prefix-check does not trigger, and extraction can escape dest by one directory.
		// #nosec G305 -- The joined path is guarded against path traversal.
		dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
		rel, err := filepath.Rel(dest, dstPath)
		if err != nil {
			return 0, err
		}

		// Note as these operations are platform specific, so must the slash be.
		if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
			return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
		}

archive.go:863

  • Path traversal guard misses the case where filepath.Rel returns exactly ".." (e.g., a tar entry name of ".." or "../"). In that case the current prefix-check does not trigger, and extraction can escape dest by one directory.
		// #nosec G305 -- The joined path is checked for path traversal.
		dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
		rel, err := filepath.Rel(dest, dstPath)
		if err != nil {
			return err
		}
		if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
			return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
		}

Comment thread diff.go Outdated
Comment on lines 93 to 95
if strings.HasPrefix(hdr.Name, WhiteoutLinkDir) && hdr.Typeflag == tar.TypeReg {
basename := filepath.Base(hdr.Name)
basename := path.Base(hdr.Name)
aufsHardlinks[basename] = hdr
Comment thread diff.go Outdated
Comment thread archive.go Outdated
Comment on lines +835 to +847
// Strip a leading "/" so absolute entries stay root-relative, and
// normalize the POSIX tar path. Skip entries referring to the extraction
// root and reject paths that escape it.
name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
if name == "." {
continue
}
for _, exclude := range options.ExcludePatterns {
if strings.HasPrefix(hdr.Name, exclude) {
if strings.HasPrefix(name, exclude) {
continue loop
}
}
hdr.Name = name
Copilot AI review requested due to automatic review settings July 21, 2026 23:25
Normalize tar entry names with path.Clean instead of filepath.Clean.

Tar header paths always use POSIX ('/') separators. Preserve their POSIX
form while operating on archive metadata, deferring conversion to native
paths until the filesystem boundary.

This fixes a bug where ExcludePatterns could fail to match valid archive
entries on Windows because filepath.Clean converted tar paths to native
path separators before exclusion matching.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Normalize tar entry names with path.Clean instead of filepath.Clean.

Tar header paths always use POSIX ('/') separators. Preserve their POSIX
form while operating on archive metadata, deferring conversion to native
paths until the filesystem boundary.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

diff.go:116

  • Path traversal guard misses the case where filepath.Rel returns exactly ".." (e.g., tar entry name ".." or "../"). That would allow writing to dest's parent because the current check only matches ".." followed by a path separator.
		dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
		rel, err := filepath.Rel(dest, dstPath)
		if err != nil {
			return 0, err
		}

archive.go:842

  • The new POSIX normalization + exclusion matching behavior isn’t covered by a unit test. Consider adding a test tar stream containing a nested header name like "dir/file" and verifying ExcludePatterns (e.g., "dir") still excludes it on all platforms (this was the reported Windows regression).
		name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
		if name == "." {
			continue
		}
		for _, exclude := range options.ExcludePatterns {

Comment thread archive.go
Comment on lines 855 to 859
// #nosec G305 -- The joined path is checked for path traversal.
dstPath := filepath.Join(dest, hdr.Name)
dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
rel, err := filepath.Rel(dest, dstPath)
if err != nil {
return err
Copilot AI review requested due to automatic review settings July 21, 2026 23:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

archive.go:859

  • The path-traversal guard below misses the case where filepath.Rel(dest, dstPath) returns exactly ".." (e.g., a tar entry named ".."), which can escape the extraction root without triggering the current HasPrefix check. Consider rejecting rel == ".." in addition to ".."+PathSeparator.
		// #nosec G305 -- The joined path is checked for path traversal.
		dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
		rel, err := filepath.Rel(dest, dstPath)
		if err != nil {
			return err

Comment thread diff.go
Comment on lines 111 to 115
// #nosec G305 -- The joined path is guarded against path traversal.
dstPath := filepath.Join(dest, hdr.Name)
dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
rel, err := filepath.Rel(dest, dstPath)
if err != nil {
return 0, err
Use filepath.Localize when converting AUFS hardlink basenames from tar
header paths to native filesystem paths. This validates that the
basename is a single native path component before it is passed to
filepath.Join(), avoiding reinterpretation of Windows path separators.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Copilot AI review requested due to automatic review settings July 22, 2026 00:09
Comment thread diff.go
defer os.RemoveAll(aufsTempdir)
}
if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, options); err != nil {
if err := createTarFile(filepath.Join(aufsTempdir, localBasename), dest, hdr, tr, options); err != nil {
Only treat hardlinks targeting entries inside /.wh..wh.plnk as AUFS
metadata by requiring a path-component boundary after the directory
name. This avoids matching similarly prefixed paths while preserving
existing handling of AUFS metadata entries and opaque whiteouts.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

archive.go:851

  • createImpliedDirectories is called before verifying that the entry stays within dest. For a crafted name like "../newdir/file", this can create directories outside dest (via filepath.Join(dest, parent)) even though the later Rel() check will reject the entry. Also, the Rel() guard should reject rel == "..". Consider validating the destination path first, then creating implied directories only after the breakout check passes.
		hdr.Name = name

		// Ensure that the parent directory exists.
		err = createImpliedDirectories(dest, hdr, options)
		if err != nil {

Comment thread diff.go
Comment on lines +52 to +59
// Strip any leading "/" so absolute entries stay root-relative, and
// normalize the POSIX tar path. Skip entries referring to the extraction
// root and reject paths that escape it.
name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
if name == "." {
continue
}
hdr.Name = name
Copilot AI review requested due to automatic review settings July 22, 2026 00:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

diff.go:59

  • The new POSIX normalization doesn’t actually reject paths that escape the extraction root (e.g., ".." or "../..."). Because createImpliedDirectories runs before the filepath.Rel breakout check, a crafted entry could still write outside dest (and the later rel-prefix check misses the exact ".." case). Add an explicit guard on the cleaned POSIX name before assigning hdr.Name.
		name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
		if name == "." {
			continue
		}
		hdr.Name = name

archive.go:841

  • The new POSIX normalization doesn’t actually reject paths that escape the extraction root (e.g., ".." or "../..."). Because createImpliedDirectories runs before the filepath.Rel breakout check, a crafted entry could still create/write outside dest. Add an explicit guard on the cleaned POSIX name before the exclude-pattern loop.
		name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
		if name == "." {
			continue
		}

archive.go:845

  • This change is intended to fix ExcludePatterns mismatches on Windows by preserving POSIX separators during archive-metadata operations, but there isn’t a regression test covering a nested path exclusion like "dir/sub" on Windows (where filepath.Clean previously rewrote "/" to "\"). Adding a Windows-focused test would prevent reintroducing the bug.
		for _, exclude := range options.ExcludePatterns {
			if strings.HasPrefix(name, exclude) {
				continue loop
			}

diff.go:118

  • The breakout/path-traversal check below misses the case where rel == ".." (no trailing separator). A tar entry named ".." would resolve to dest’s parent and bypass the current prefix-only guard, so it should be rejected explicitly.
		dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
		rel, err := filepath.Rel(dest, dstPath)
		if err != nil {

archive.go:858

  • The breakout/path-traversal check below misses the case where rel == ".." (no trailing separator). A tar entry named ".." would resolve to dest’s parent and bypass the current prefix-only guard, so it should be rejected explicitly.
		dstPath := filepath.Join(dest, filepath.FromSlash(hdr.Name))
		rel, err := filepath.Rel(dest, dstPath)
		if err != nil {

@thaJeztah
thaJeztah marked this pull request as draft July 22, 2026 09:30
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.

4 participants