Skip to content

Commit a88c068

Browse files
Allow for specifying file permissions when untarring bundle (#61)
1 parent 3d14b08 commit a88c068

File tree

2 files changed

+202
-14
lines changed

2 files changed

+202
-14
lines changed

fsutil/filesystem.go

+68-14
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ package fsutil
44
import (
55
"archive/tar"
66
"compress/gzip"
7+
"fmt"
78
"io"
9+
"io/fs"
810
"os"
911
"path/filepath"
1012
"strings"
1113

1214
"github.com/kolide/kit/env"
13-
"github.com/pkg/errors"
1415
)
1516

1617
const (
@@ -93,13 +94,13 @@ func CopyFile(src, dest string) error {
9394
func UntarBundle(destination string, source string) error {
9495
f, err := os.Open(source)
9596
if err != nil {
96-
return errors.Wrap(err, "open download source")
97+
return fmt.Errorf("opening source: %w", err)
9798
}
9899
defer f.Close()
99100

100101
gzr, err := gzip.NewReader(f)
101102
if err != nil {
102-
return errors.Wrapf(err, "create gzip reader from %s", source)
103+
return fmt.Errorf("creating gzip reader from %s: %w", source, err)
103104
}
104105
defer gzr.Close()
105106

@@ -110,40 +111,93 @@ func UntarBundle(destination string, source string) error {
110111
break
111112
}
112113
if err != nil {
113-
return errors.Wrap(err, "reading tar file")
114+
return fmt.Errorf("reading tar file: %w", err)
114115
}
115116

116117
if err := sanitizeExtractPath(filepath.Dir(destination), header.Name); err != nil {
117-
return errors.Wrap(err, "checking filename")
118+
return fmt.Errorf("checking filename: %w", err)
118119
}
119120

120-
path := filepath.Join(filepath.Dir(destination), header.Name)
121+
destPath := filepath.Join(filepath.Dir(destination), header.Name)
121122
info := header.FileInfo()
122123
if info.IsDir() {
123-
if err = os.MkdirAll(path, info.Mode()); err != nil {
124-
return errors.Wrapf(err, "creating directory for tar file: %s", path)
124+
if err = os.MkdirAll(destPath, info.Mode()); err != nil {
125+
return fmt.Errorf("creating directory %s for tar file: %w", destPath, err)
125126
}
126127
continue
127128
}
128129

129-
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
130+
if err := writeBundleFile(destPath, info.Mode(), tr); err != nil {
131+
return fmt.Errorf("writing file: %w", err)
132+
}
133+
}
134+
return nil
135+
}
136+
137+
// UntarBundleWithRequiredFilePermission performs the same operation as UntarBundle,
138+
// but enforces `requiredFilePerm` for all files in the bundle.
139+
func UntarBundleWithRequiredFilePermission(destination string, source string, requiredFilePerm fs.FileMode) error {
140+
f, err := os.Open(source)
141+
if err != nil {
142+
return fmt.Errorf("opening source: %w", err)
143+
}
144+
defer f.Close()
145+
146+
gzr, err := gzip.NewReader(f)
147+
if err != nil {
148+
return fmt.Errorf("creating gzip reader from %s: %w", source, err)
149+
}
150+
defer gzr.Close()
151+
152+
tr := tar.NewReader(gzr)
153+
for {
154+
header, err := tr.Next()
155+
if err == io.EOF {
156+
break
157+
}
130158
if err != nil {
131-
return errors.Wrapf(err, "open file %s", path)
159+
return fmt.Errorf("reading tar file: %w", err)
160+
}
161+
162+
if err := sanitizeExtractPath(filepath.Dir(destination), header.Name); err != nil {
163+
return fmt.Errorf("checking filename: %w", err)
132164
}
133-
defer file.Close()
134-
if _, err := io.Copy(file, tr); err != nil {
135-
return errors.Wrapf(err, "copy tar %s to destination %s", header.FileInfo().Name(), path)
165+
166+
destPath := filepath.Join(filepath.Dir(destination), header.Name)
167+
info := header.FileInfo()
168+
if info.IsDir() {
169+
if err = os.MkdirAll(destPath, info.Mode()); err != nil {
170+
return fmt.Errorf("creating directory %s for tar file: %w", destPath, err)
171+
}
172+
continue
173+
}
174+
175+
if err := writeBundleFile(destPath, requiredFilePerm, tr); err != nil {
176+
return fmt.Errorf("writing file: %w", err)
136177
}
137178
}
138179
return nil
139180
}
140181

182+
func writeBundleFile(destPath string, perm fs.FileMode, srcReader io.Reader) error {
183+
file, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
184+
if err != nil {
185+
return fmt.Errorf("opening %s: %w", destPath, err)
186+
}
187+
defer file.Close()
188+
if _, err := io.Copy(file, srcReader); err != nil {
189+
return fmt.Errorf("copying to %s: %w", destPath, err)
190+
}
191+
192+
return nil
193+
}
194+
141195
// sanitizeExtractPath checks that the supplied extraction path is nor
142196
// vulnerable to zip slip attacks. See https://snyk.io/research/zip-slip-vulnerability
143197
func sanitizeExtractPath(filePath string, destination string) error {
144198
destpath := filepath.Join(destination, filePath)
145199
if !strings.HasPrefix(destpath, filepath.Clean(destination)+string(os.PathSeparator)) {
146-
return errors.Errorf("%s: illegal file path", filePath)
200+
return fmt.Errorf("%s: illegal file path", filePath)
147201
}
148202
return nil
149203
}

fsutil/filesystem_test.go

+134
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,145 @@
11
package fsutil
22

33
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"path/filepath"
11+
"strings"
412
"testing"
513

614
"github.com/stretchr/testify/require"
715
)
816

17+
func TestUntarBundle(t *testing.T) {
18+
t.Parallel()
19+
20+
// Create tarball contents
21+
originalDir := t.TempDir()
22+
topLevelFile := filepath.Join(originalDir, "testfile.txt")
23+
var topLevelFileMode fs.FileMode = 0655
24+
require.NoError(t, os.WriteFile(topLevelFile, []byte("test1"), topLevelFileMode))
25+
internalDir := filepath.Join(originalDir, "some", "path", "to")
26+
var nestedFileMode fs.FileMode = 0755
27+
require.NoError(t, os.MkdirAll(internalDir, nestedFileMode))
28+
nestedFile := filepath.Join(internalDir, "anotherfile.txt")
29+
require.NoError(t, os.WriteFile(nestedFile, []byte("test2"), nestedFileMode))
30+
31+
// Create test tarball
32+
tarballDir := t.TempDir()
33+
tarballFile := filepath.Join(tarballDir, "test.gz")
34+
createTar(t, tarballFile, originalDir)
35+
36+
// Confirm we can untar the tarball successfully
37+
newDir := t.TempDir()
38+
require.NoError(t, UntarBundle(filepath.Join(newDir, "anything"), tarballFile))
39+
40+
// Confirm the tarball has the contents we expect
41+
newTopLevelFile := filepath.Join(newDir, filepath.Base(topLevelFile))
42+
require.FileExists(t, newTopLevelFile)
43+
newNestedFile := filepath.Join(newDir, "some", "path", "to", filepath.Base(nestedFile))
44+
require.FileExists(t, newNestedFile)
45+
46+
// Confirm each file retained its original permissions
47+
topLevelFileInfo, err := os.Stat(newTopLevelFile)
48+
require.NoError(t, err)
49+
require.Equal(t, topLevelFileMode, topLevelFileInfo.Mode())
50+
nestedFileInfo, err := os.Stat(newNestedFile)
51+
require.NoError(t, err)
52+
require.Equal(t, nestedFileMode, nestedFileInfo.Mode())
53+
}
54+
55+
func TestUntarBundleWithRequiredFilePermission(t *testing.T) {
56+
t.Parallel()
57+
58+
// Create tarball contents
59+
originalDir := t.TempDir()
60+
topLevelFile := filepath.Join(originalDir, "testfile.txt")
61+
require.NoError(t, os.WriteFile(topLevelFile, []byte("test1"), 0655))
62+
internalDir := filepath.Join(originalDir, "some", "path", "to")
63+
require.NoError(t, os.MkdirAll(internalDir, 0744))
64+
nestedFile := filepath.Join(internalDir, "anotherfile.txt")
65+
require.NoError(t, os.WriteFile(nestedFile, []byte("test2"), 0744))
66+
67+
// Create test tarball
68+
tarballDir := t.TempDir()
69+
tarballFile := filepath.Join(tarballDir, "test.gz")
70+
createTar(t, tarballFile, originalDir)
71+
72+
// Confirm we can untar the tarball successfully
73+
newDir := t.TempDir()
74+
var requiredFileMode fs.FileMode = 0755
75+
require.NoError(t, UntarBundleWithRequiredFilePermission(filepath.Join(newDir, "anything"), tarballFile, requiredFileMode))
76+
77+
// Confirm the tarball has the contents we expect
78+
newTopLevelFile := filepath.Join(newDir, filepath.Base(topLevelFile))
79+
require.FileExists(t, newTopLevelFile)
80+
newNestedFile := filepath.Join(newDir, "some", "path", "to", filepath.Base(nestedFile))
81+
require.FileExists(t, newNestedFile)
82+
83+
// Require that both files have the required permission 0755
84+
topLevelFileInfo, err := os.Stat(newTopLevelFile)
85+
require.NoError(t, err)
86+
require.Equal(t, requiredFileMode, topLevelFileInfo.Mode())
87+
nestedFileInfo, err := os.Stat(newNestedFile)
88+
require.NoError(t, err)
89+
require.Equal(t, requiredFileMode, nestedFileInfo.Mode())
90+
}
91+
92+
// createTar is a helper to create a test tar
93+
func createTar(t *testing.T, createLocation string, sourceDir string) {
94+
tarballFile, err := os.Create(createLocation)
95+
require.NoError(t, err)
96+
defer tarballFile.Close()
97+
98+
gzw := gzip.NewWriter(tarballFile)
99+
defer gzw.Close()
100+
101+
tw := tar.NewWriter(gzw)
102+
defer tw.Close()
103+
104+
require.NoError(t, filepath.Walk(sourceDir, func(path string, info fs.FileInfo, err error) error {
105+
if err != nil {
106+
return err
107+
}
108+
109+
srcInfo, err := os.Lstat(path)
110+
if os.IsNotExist(err) {
111+
return fmt.Errorf("error adding %s to tarball: %w", path, err)
112+
}
113+
114+
hdr, err := tar.FileInfoHeader(srcInfo, path)
115+
if err != nil {
116+
return fmt.Errorf("error creating tar header: %w", err)
117+
}
118+
hdr.Name = strings.TrimPrefix(path, sourceDir+"/")
119+
120+
if err := tw.WriteHeader(hdr); err != nil {
121+
return fmt.Errorf("error writing tar header: %w", err)
122+
}
123+
124+
if !srcInfo.Mode().IsRegular() {
125+
// Don't open/copy over directories
126+
return nil
127+
}
128+
129+
srcFile, err := os.Open(path)
130+
if err != nil {
131+
return fmt.Errorf("error opening file to add to tarball: %w", err)
132+
}
133+
defer srcFile.Close()
134+
135+
if _, err := io.Copy(tw, srcFile); err != nil {
136+
return fmt.Errorf("error copying file %s to tarball: %w", path, err)
137+
}
138+
139+
return nil
140+
}))
141+
}
142+
9143
func TestSanitizeExtractPath(t *testing.T) {
10144
t.Parallel()
11145

0 commit comments

Comments
 (0)