Skip to content

Commit 28cafc5

Browse files
authored
Merge pull request #12 from hashicorp/b-zipslip
Fix for unsafe tar unpacking
2 parents dbc66eb + a3957ec commit 28cafc5

File tree

2 files changed

+208
-18
lines changed

2 files changed

+208
-18
lines changed

slug.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,44 @@ func Unpack(r io.Reader, dst string) error {
230230
}
231231
path = filepath.Join(dst, path)
232232

233+
// Check for paths outside our directory, they are forbidden
234+
target := filepath.Clean(path)
235+
if !strings.HasPrefix(target, dst) {
236+
return fmt.Errorf("Invalid filename, traversal with \"..\" outside of current directory")
237+
}
238+
239+
// Ensure the destination is not through any symlinks. This prevents
240+
// any files from being deployed through symlinks defined in the slug.
241+
// There are malicious cases where this could be used to escape the
242+
// slug's boundaries (zipslip), and any legitimate use is questionable
243+
// and likely indicates a hand-crafted tar file, which we are not in
244+
// the business of supporting here.
245+
//
246+
// The strategy is to Lstat each path component from dst up to the
247+
// immediate parent directory of the file name in the tarball, checking
248+
// the mode on each to ensure we wouldn't be passing through any
249+
// symlinks.
250+
currentPath := dst // Start at the root of the unpacked tarball.
251+
components := strings.Split(header.Name, "/")
252+
253+
for i := 0; i < len(components)-1; i++ {
254+
currentPath = filepath.Join(currentPath, components[i])
255+
fi, err := os.Lstat(currentPath)
256+
if os.IsNotExist(err) {
257+
// Parent directory structure is incomplete. Technically this
258+
// means from here upward cannot be a symlink, so we cancel the
259+
// remaining path tests.
260+
break
261+
}
262+
if err != nil {
263+
return fmt.Errorf("Failed to evaluate path %q: %v", header.Name, err)
264+
}
265+
if fi.Mode()&os.ModeSymlink != 0 {
266+
return fmt.Errorf("Cannot extract %q through symlink",
267+
header.Name)
268+
}
269+
}
270+
233271
// Make the directories to the path.
234272
dir := filepath.Dir(path)
235273
if err := os.MkdirAll(dir, 0755); err != nil {

slug_test.go

Lines changed: 170 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -392,24 +392,104 @@ func TestUnpackErrorOnUnhandledType(t *testing.T) {
392392

393393
func TestUnpackMaliciousSymlinks(t *testing.T) {
394394
tcases := []struct {
395-
desc string
396-
target string
397-
err string
395+
desc string
396+
headers []*tar.Header
397+
err string
398398
}{
399399
{
400-
desc: "symlink with absolute path",
401-
target: "/etc/shadow",
402-
err: "has absolute target",
400+
desc: "symlink with absolute path",
401+
headers: []*tar.Header{
402+
&tar.Header{
403+
Name: "l",
404+
Linkname: "/etc/shadow",
405+
Typeflag: tar.TypeSymlink,
406+
},
407+
},
408+
err: "has absolute target",
403409
},
404410
{
405-
desc: "symlink with external target",
406-
target: "../../../../../etc/shadow",
407-
err: "has external target",
411+
desc: "symlink with external target",
412+
headers: []*tar.Header{
413+
&tar.Header{
414+
Name: "l",
415+
Linkname: "../../../../../etc/shadow",
416+
Typeflag: tar.TypeSymlink,
417+
},
418+
},
419+
err: "has external target",
408420
},
409421
{
410-
desc: "symlink with nested external target",
411-
target: "foo/bar/baz/../../../../../../../../etc/shadow",
412-
err: "has external target",
422+
desc: "symlink with nested external target",
423+
headers: []*tar.Header{
424+
&tar.Header{
425+
Name: "l",
426+
Linkname: "foo/bar/baz/../../../../../../../../etc/shadow",
427+
Typeflag: tar.TypeSymlink,
428+
},
429+
},
430+
err: "has external target",
431+
},
432+
{
433+
desc: "zipslip vulnerability",
434+
headers: []*tar.Header{
435+
&tar.Header{
436+
Name: "subdir/parent",
437+
Linkname: "..",
438+
Typeflag: tar.TypeSymlink,
439+
},
440+
&tar.Header{
441+
Name: "subdir/parent/escapes",
442+
Linkname: "..",
443+
Typeflag: tar.TypeSymlink,
444+
},
445+
},
446+
err: `Cannot extract "subdir/parent/escapes" through symlink`,
447+
},
448+
{
449+
desc: "nested symlinks within symlinked dir",
450+
headers: []*tar.Header{
451+
&tar.Header{
452+
Name: "subdir/parent",
453+
Linkname: "..",
454+
Typeflag: tar.TypeSymlink,
455+
},
456+
&tar.Header{
457+
Name: "subdir/parent/otherdir/escapes",
458+
Linkname: "../..",
459+
Typeflag: tar.TypeSymlink,
460+
},
461+
},
462+
err: `Cannot extract "subdir/parent/otherdir/escapes" through symlink`,
463+
},
464+
{
465+
desc: "regular file through symlink",
466+
headers: []*tar.Header{
467+
&tar.Header{
468+
Name: "subdir/parent",
469+
Linkname: "..",
470+
Typeflag: tar.TypeSymlink,
471+
},
472+
&tar.Header{
473+
Name: "subdir/parent/file",
474+
Typeflag: tar.TypeReg,
475+
},
476+
},
477+
err: `Cannot extract "subdir/parent/file" through symlink`,
478+
},
479+
{
480+
desc: "directory through symlink",
481+
headers: []*tar.Header{
482+
&tar.Header{
483+
Name: "subdir/parent",
484+
Linkname: "..",
485+
Typeflag: tar.TypeSymlink,
486+
},
487+
&tar.Header{
488+
Name: "subdir/parent/dir",
489+
Typeflag: tar.TypeDir,
490+
},
491+
},
492+
err: `Cannot extract "subdir/parent/dir" through symlink`,
413493
},
414494
}
415495

@@ -435,14 +515,86 @@ func TestUnpackMaliciousSymlinks(t *testing.T) {
435515
// Tar the file contents
436516
tarW := tar.NewWriter(gzipW)
437517

438-
var hdr tar.Header
518+
for _, hdr := range tc.headers {
519+
tarW.WriteHeader(hdr)
520+
}
521+
522+
tarW.Close()
523+
gzipW.Close()
524+
wfh.Close()
439525

440-
hdr.Typeflag = tar.TypeSymlink
441-
hdr.Name = "l"
442-
hdr.Size = int64(0)
443-
hdr.Linkname = tc.target
526+
// Open the slug file for reading.
527+
fh, err := os.Open(in)
528+
if err != nil {
529+
t.Fatalf("err: %v", err)
530+
}
444531

445-
tarW.WriteHeader(&hdr)
532+
// Create a dir to unpack into.
533+
dst, err := ioutil.TempDir(dir, "")
534+
if err != nil {
535+
t.Fatalf("err: %v", err)
536+
}
537+
defer os.RemoveAll(dst)
538+
539+
// Now try unpacking it, which should fail
540+
err = Unpack(fh, dst)
541+
if err == nil || !strings.Contains(err.Error(), tc.err) {
542+
t.Fatalf("expected %v, got %v", tc.err, err)
543+
}
544+
})
545+
}
546+
}
547+
548+
func TestUnpackMaliciousFiles(t *testing.T) {
549+
tcases := []struct {
550+
desc string
551+
name string
552+
err string
553+
}{
554+
{
555+
desc: "filename containing path traversal",
556+
name: "../../../../../../../../tmp/test",
557+
err: "Invalid filename, traversal with \"..\" outside of current directory",
558+
},
559+
{
560+
desc: "should fail before attempting to create directories",
561+
name: "../../../../../../../../Users/root",
562+
err: "Invalid filename, traversal with \"..\" outside of current directory",
563+
},
564+
}
565+
566+
for _, tc := range tcases {
567+
t.Run(tc.desc, func(t *testing.T) {
568+
dir, err := ioutil.TempDir("", "slug")
569+
if err != nil {
570+
t.Fatalf("err:%v", err)
571+
}
572+
defer os.RemoveAll(dir)
573+
in := filepath.Join(dir, "slug.tar.gz")
574+
575+
// Create the output file
576+
wfh, err := os.Create(in)
577+
if err != nil {
578+
t.Fatalf("err: %v", err)
579+
}
580+
581+
// Gzip compress all the output data
582+
gzipW := gzip.NewWriter(wfh)
583+
584+
// Tar the file contents
585+
tarW := tar.NewWriter(gzipW)
586+
587+
hdr := &tar.Header{
588+
Name: tc.name,
589+
Mode: 0600,
590+
Size: int64(0),
591+
}
592+
if err := tarW.WriteHeader(hdr); err != nil {
593+
t.Fatalf("err: %v", err)
594+
}
595+
if _, err := tarW.Write([]byte{}); err != nil {
596+
t.Fatalf("err: %v", err)
597+
}
446598

447599
tarW.Close()
448600
gzipW.Close()

0 commit comments

Comments
 (0)