Skip to content

Commit b11f777

Browse files
authored
Merge commit from fork
ensureLinkPath validated TypeLink targets by resolving relative paths against the link file's directory, but returned the original (unresolved) target string. The caller passed this to os.Link, which resolves relative paths against the process CWD via link(2), not the link file's directory. A crafted OCI artifact could exploit this to hardlink a CWD file into the extract tree (GHSA-fxhp-mv3v-67qp). Fix: explicitly resolve relative hardlink targets against filepath.Dir of the link file before calling os.Link. Symlink handling is unaffected since os.Symlink stores the path verbatim and resolves at access time. Add a regression test that sets the process CWD to a directory containing a sentinel file and verifies that a tar TypeLink entry with a relative Linkname cannot hardlink that file into the extract tree. Signed-off-by: Terry Howe <terrylhowe@gmail.com>
1 parent 9fc3dfb commit b11f777

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

content/file/utils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte, prese
188188
// This is a known limitation and will not be addressed.
189189
var target string
190190
if target, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname); err == nil {
191+
if !filepath.IsAbs(target) {
192+
// link(2) resolves relative paths against the process CWD, not
193+
// the link file's directory. Resolve explicitly to prevent escape.
194+
target = filepath.Join(filepath.Dir(filePath), target)
195+
}
191196
err = os.Link(target, filePath)
192197
}
193198
case tar.TypeSymlink:

content/file/utils_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,61 @@ func Test_extractTarDirectory_HardLink(t *testing.T) {
376376
t.Error("extractTarDirectory() error = nil, wantErr = true")
377377
}
378378
})
379+
380+
t.Run("hard link with relative linkname must not escape extract dir via process CWD", func(t *testing.T) {
381+
// GHSA-fxhp-mv3v-67qp: a tarball TypeLink entry with a relative Linkname
382+
// was passed verbatim to os.Link, which resolves relative paths against the
383+
// process CWD rather than the link file's directory. An attacker-controlled
384+
// registry could use this to hardlink a CWD file into the extract tree.
385+
cwdDir := t.TempDir()
386+
sentinelPath := filepath.Join(cwdDir, "sentinel.txt")
387+
if err := os.WriteFile(sentinelPath, []byte("secret"), 0600); err != nil {
388+
t.Fatal(err)
389+
}
390+
391+
origDir, err := os.Getwd()
392+
if err != nil {
393+
t.Fatal(err)
394+
}
395+
if err := os.Chdir(cwdDir); err != nil {
396+
t.Fatal(err)
397+
}
398+
defer os.Chdir(origDir) //nolint:errcheck
399+
400+
extractDir := t.TempDir()
401+
dirName := "base"
402+
dirPath := filepath.Join(extractDir, dirName)
403+
buf := make([]byte, 1024)
404+
405+
// The relative Linkname "sentinel.txt" would resolve against the process
406+
// CWD (cwdDir) via link(2) if not explicitly resolved first.
407+
tarData := createTar(t, []tarEntry{
408+
{name: "base/", mode: os.ModeDir | 0777},
409+
{name: "base/evil_link", linkname: "sentinel.txt", mode: 0666, isHardLink: true},
410+
})
411+
412+
err = extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf, false)
413+
if err != nil {
414+
// Expected: target resolves to <extractDir>/base/sentinel.txt which
415+
// doesn't exist, so os.Link returns an error. No escape occurred.
416+
return
417+
}
418+
419+
// If extraction succeeded, verify the hardlink does not share an inode
420+
// with the CWD sentinel file (i.e., it did not escape the extract dir).
421+
evilLinkPath := filepath.Join(dirPath, "evil_link")
422+
evilInfo, statErr := os.Lstat(evilLinkPath)
423+
if statErr != nil {
424+
return // link was not created; no escape
425+
}
426+
sentinelInfo, statErr := os.Lstat(sentinelPath)
427+
if statErr != nil {
428+
t.Fatal(statErr)
429+
}
430+
if os.SameFile(evilInfo, sentinelInfo) {
431+
t.Error("hardlink escaped extract dir: evil_link shares inode with CWD sentinel file")
432+
}
433+
})
379434
}
380435

381436
type tarEntry struct {

0 commit comments

Comments
 (0)