Skip to content

Commit 909adc8

Browse files
committed
add fix for zip slip siva vulnerability
* document `IndexEntry.Name`, including security notes. * `siva unpack` now fails if it would result in a file being extracted outside the output directory. * `Index.ToSafePaths` convenience function is provided for users who write siva extraction code and want a quick way to make it safe. Thanks to Toni Cárdenas (@tcard) for notifying us about the vulnerability and proposing a fixed. Signed-off-by: Santiago M. Mola <[email protected]>
1 parent 3fae810 commit 909adc8

File tree

8 files changed

+174
-19
lines changed

8 files changed

+174
-19
lines changed

cmd/siva/pack.go

+1-18
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (c *CmdPack) packFile(fullpath string, fi os.FileInfo) error {
117117

118118
func (c *CmdPack) writeFileHeader(fullpath string, fi os.FileInfo) error {
119119
h := &siva.Header{
120-
Name: cleanPath(fullpath),
120+
Name: siva.ToSafePath(fullpath),
121121
Mode: fi.Mode(),
122122
ModTime: fi.ModTime(),
123123
}
@@ -151,20 +151,3 @@ func (c *CmdPack) writeFile(fullpath string, fi os.FileInfo) error {
151151

152152
return c.w.Flush()
153153
}
154-
155-
func cleanPath(path string) string {
156-
path = filepath.Clean(path)
157-
for len(path) >= 3 && path[:3] == "../" {
158-
path = path[3:]
159-
}
160-
161-
for len(path) >= 2 && path[:2] == "./" {
162-
path = path[2:]
163-
}
164-
165-
if len(path) > 1 && path[:1] == "/" {
166-
path = path[1:]
167-
}
168-
169-
return path
170-
}

cmd/siva/pack_test.go

+39-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func Test(t *testing.T) { TestingT(t) }
1616
type PackSuite struct {
1717
folder string
1818
files []string
19+
cwd string
1920
}
2021

2122
var _ = Suite(&PackSuite{})
@@ -31,16 +32,22 @@ func (s *PackSuite) SetUpTest(c *C) {
3132
s.files = []string{}
3233
for _, f := range files {
3334
target := filepath.Join(s.folder, "files", f.Name)
34-
err := ioutil.WriteFile(target, []byte(f.Body), 0666)
35+
err = ioutil.WriteFile(target, []byte(f.Body), 0666)
3536
c.Assert(err, IsNil)
3637

3738
s.files = append(s.files, target)
3839
}
40+
41+
cwd, err := os.Getwd()
42+
c.Assert(err, IsNil)
43+
s.cwd = cwd
3944
}
4045

4146
func (s *PackSuite) TearDownTest(c *C) {
4247
err := os.RemoveAll(s.folder)
4348
c.Assert(err, IsNil)
49+
err = os.Chdir(s.cwd)
50+
c.Assert(err, IsNil)
4451
}
4552

4653
func (s *PackSuite) TestValidate(c *C) {
@@ -125,6 +132,37 @@ func (s *PackSuite) TestAppend(c *C) {
125132
c.Assert(f.Close(), IsNil)
126133
}
127134

135+
func (s *PackSuite) TestCleanPaths(c *C) {
136+
cmd := &CmdPack{}
137+
138+
subdir := filepath.Join(s.folder, "files", "subdir")
139+
err := os.Mkdir(subdir, 0766)
140+
c.Assert(err, IsNil)
141+
err = os.Chdir(subdir)
142+
c.Assert(err, IsNil)
143+
144+
cmd.Args.File = filepath.Join(s.folder, "foo.siva")
145+
cmd.Input.Files = []string{filepath.Join("..", "gopher.txt")}
146+
147+
err = cmd.Execute(nil)
148+
c.Assert(err, IsNil)
149+
150+
f, err := os.Open(cmd.Args.File)
151+
c.Assert(err, IsNil)
152+
153+
fi, err := f.Stat()
154+
c.Assert(err, IsNil)
155+
c.Assert(int(fi.Size()), Equals, 113)
156+
157+
r := siva.NewReader(f)
158+
i, err := r.Index()
159+
c.Assert(err, IsNil)
160+
c.Assert(i, HasLen, 1)
161+
entry := i.Find("gopher.txt")
162+
c.Assert(entry, NotNil)
163+
c.Assert(entry.Name, Equals, "gopher.txt")
164+
}
165+
128166
type fileFixture struct {
129167
Name, Body string
130168
}

cmd/siva/unpack.go

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path/filepath"
88
"regexp"
9+
"strings"
910

1011
"gopkg.in/src-d/go-siva.v1"
1112

@@ -128,6 +129,10 @@ func (c *CmdUnpack) extract(entry *siva.IndexEntry) error {
128129
func (c *CmdUnpack) createFile(entry *siva.IndexEntry) (*os.File, error) {
129130
dstName := filepath.Join(c.Output.Path, entry.Name)
130131

132+
if err := c.checkSafePath(c.Output.Path, dstName); err != nil {
133+
return nil, err
134+
}
135+
131136
dir := filepath.Dir(dstName)
132137
if err := os.MkdirAll(dir, 0755); err != nil {
133138
return nil, fmt.Errorf("unable to create dir %q: %s\n", dir, err)
@@ -145,3 +150,19 @@ func (c *CmdUnpack) createFile(entry *siva.IndexEntry) (*os.File, error) {
145150

146151
return dst, nil
147152
}
153+
154+
func (c *CmdUnpack) checkSafePath(base, target string) error {
155+
rel, err := filepath.Rel(base, target)
156+
if err != nil {
157+
return fmt.Errorf("target path (%s) is not relative to base (%s): %s\n",
158+
target, base, err)
159+
}
160+
161+
rel = filepath.ToSlash(rel)
162+
if strings.HasPrefix(rel, "../") {
163+
return fmt.Errorf("target path (%s) outside base (%s) is not allowed",
164+
target, base)
165+
}
166+
167+
return nil
168+
}

cmd/siva/unpack_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,16 @@ func (s *UnpackSuite) TestOverwrite(c *C) {
9595
c.Assert(err, IsNil)
9696
c.Assert(dir, HasLen, 3)
9797
}
98+
99+
func (s *UnpackSuite) TestZipSlip(c *C) {
100+
cmd := &CmdUnpack{}
101+
cmd.Output.Path = filepath.Join(s.folder, "files/inside")
102+
cmd.Args.File = "../../fixtures/zipslip.siva"
103+
104+
err := cmd.Execute(nil)
105+
c.Assert(err, NotNil)
106+
107+
_, err = os.Stat(filepath.Join(s.folder, "files"))
108+
c.Assert(err, NotNil)
109+
c.Assert(os.IsNotExist(err), Equals, true)
110+
}

common.go

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ const (
1919

2020
// Header contains the meta information from a file
2121
type Header struct {
22+
// Name is an arbitrary UTF-8 string identifying a file in the archive. Note
23+
// that this might or might not be a POSIX-compliant path.
24+
//
25+
// Security note: Users should be careful when using name as a file path
26+
// (e.g. to extract an archive) since it can contain relative paths and be
27+
// vulnerable to Zip Slip (https://snyk.io/research/zip-slip-vulnerability)
28+
// or other potentially dangerous values such as absolute paths, network
29+
// drive addresses, etc.
2230
Name string
2331
ModTime time.Time
2432
Mode os.FileMode

fixtures/zipslip.siva

94 Bytes
Binary file not shown.

index.go

+43
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"path/filepath"
1010
"sort"
11+
"strings"
1112
"time"
1213
)
1314

@@ -189,6 +190,22 @@ func (i *Index) Filter() Index {
189190
return f
190191
}
191192

193+
// ToSafePaths creates a new index where all entry names are transformed to safe
194+
// paths using the top-level `ToSafePath` function. If you are using siva to
195+
// extract files to the file-system, you should either use this function or
196+
// perform your own validation and normalization.
197+
func (i *Index) ToSafePaths() Index {
198+
f := make(Index, len(*i))
199+
200+
for idx, e := range *i {
201+
e = &*e
202+
e.Name = ToSafePath(e.Name)
203+
f[idx] = e
204+
}
205+
206+
return f
207+
}
208+
192209
// Find returns the first IndexEntry with the given name, if any
193210
func (i Index) Find(name string) *IndexEntry {
194211
for _, e := range i {
@@ -385,3 +402,29 @@ type IndexWriteError struct {
385402
func (e *IndexWriteError) Error() string {
386403
return fmt.Sprintf("index write failed: %s", e.Err.Error())
387404
}
405+
406+
// ToSafePath transforms a filesystem path to one that is safe to
407+
// use as a relative path on the native filesystem:
408+
//
409+
// - Removes drive and network share on Windows.
410+
// - Does regular clean up (removing `/./` parts).
411+
// - Removes any leading `../`.
412+
// - Removes leading `/`.
413+
//
414+
// This is a convenience function to implement siva file extractors that are not
415+
// vulnerable to zip slip and similar vulnerabilities. However, for Windows
416+
// absolute paths (with drive or network share) it does not give consistent
417+
// results across platforms.
418+
//
419+
// If your application relies on using absolute paths, you should not use this
420+
// and you are encouraged to do your own validation and normalization.
421+
func ToSafePath(path string) string {
422+
volume := filepath.VolumeName(path)
423+
if volume != "" {
424+
path = strings.Replace(path, volume, "", 1)
425+
}
426+
427+
path = filepath.Join(string(filepath.Separator), path)
428+
path = filepath.ToSlash(path)
429+
return path[1:]
430+
}

index_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package siva
22

33
import (
44
"bytes"
5+
"runtime"
56
"sort"
67
"time"
78

@@ -104,3 +105,51 @@ func (s *IndexSuite) TestFind(c *C) {
104105
c.Assert(e, NotNil)
105106
c.Assert(e.Start, Equals, uint64(2))
106107
}
108+
109+
func (s *IndexSuite) TestToSafePathsWindows(c *C) {
110+
if runtime.GOOS != "windows" {
111+
c.Skip("windows only")
112+
}
113+
114+
i := Index{
115+
{Header: Header{Name: `C:\foo\bar`}, Start: 1},
116+
{Header: Header{Name: `\\network\share\foo\bar`}, Start: 2},
117+
{Header: Header{Name: `/foo/bar`}, Start: 3},
118+
{Header: Header{Name: `../bar`}, Start: 4},
119+
{Header: Header{Name: `foo/bar/../../baz`}, Start: 5},
120+
}
121+
122+
f := i.ToSafePaths()
123+
expected := Index{
124+
{Header: Header{Name: `foo/bar`}, Start: 1},
125+
{Header: Header{Name: `foo/bar`}, Start: 2},
126+
{Header: Header{Name: `foo/bar`}, Start: 3},
127+
{Header: Header{Name: `bar`}, Start: 4},
128+
{Header: Header{Name: `baz`}, Start: 5},
129+
}
130+
c.Assert(f, DeepEquals, expected)
131+
}
132+
133+
func (s *IndexSuite) TestToSafePathsNotWindows(c *C) {
134+
if runtime.GOOS == "windows" {
135+
c.Skip("posix only")
136+
}
137+
138+
i := Index{
139+
{Header: Header{Name: `C:\foo\bar`}, Start: 1},
140+
{Header: Header{Name: `\\network\share\foo\bar`}, Start: 2},
141+
{Header: Header{Name: `/foo/bar`}, Start: 3},
142+
{Header: Header{Name: `../bar`}, Start: 4},
143+
{Header: Header{Name: `foo/bar/../../baz`}, Start: 5},
144+
}
145+
146+
f := i.ToSafePaths()
147+
expected := Index{
148+
{Header: Header{Name: `C:\foo\bar`}, Start: 1},
149+
{Header: Header{Name: `\\network\share\foo\bar`}, Start: 2},
150+
{Header: Header{Name: `foo/bar`}, Start: 3},
151+
{Header: Header{Name: `bar`}, Start: 4},
152+
{Header: Header{Name: `baz`}, Start: 5},
153+
}
154+
c.Assert(f, DeepEquals, expected)
155+
}

0 commit comments

Comments
 (0)