• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ctfer-io / romeo / 23090073274

14 Mar 2026 02:41PM UTC coverage: 26.633%. Remained the same
23090073274

push

github

web-flow
fix: missing check on archive path sanitization (#671)

0 of 7 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

367 of 1378 relevant lines covered (26.63%)

0.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/webserver/api/v1/decoder.go
1
package apiv1
2

3
import (
4
        "archive/zip"
5
        "fmt"
6
        "io"
7
        "os"
8
        "path/filepath"
9
        "strings"
10

11
        "github.com/pkg/errors"
12
)
13

14
const (
15
        blockSize = 1 << 13 // arbitrary
16
)
17

18
// Decompressor handle the load of
19
type Decompressor struct {
20
        *Options
21
}
22

23
type Options struct {
24
        MaxSize int64
25

26
        currSize int64
27
}
28

29
// NewDecompressor constructs a fresh Decompressor.
30
func NewDecompressor(opts *Options) *Decompressor {
×
31
        if opts == nil {
×
32
                opts = &Options{}
×
33
        }
×
34
        return &Decompressor{
×
35
                Options: opts,
×
36
        }
×
37
}
38

39
// Unzip extracts the content of the zip reader into cd.
40
// It returns the directory it extracted into for Pulumi to use,
41
// or an error if anything unexpected happens.
42
func (dec *Decompressor) Unzip(r *zip.Reader, cd string) (string, error) {
×
43
        outDir := ""
×
44
        for _, f := range r.File {
×
45
                if f.FileInfo().IsDir() {
×
46
                        continue
×
47
                }
48
                filePath, err := sanitizeArchivePath(cd, f.Name)
×
49
                if err != nil {
×
50
                        return cd, err
×
51
                }
×
52

53
                // Save output directory i.e. the directory containing the Pulumi.yaml file,
54
                // the scenario entrypoint.
55
                base := filepath.Base(filePath)
×
56
                if base == "Pulumi.yaml" || base == "Pulumi.yml" {
×
57
                        if outDir != "" {
×
58
                                return cd, errors.New("archive contain multiple Pulumi yaml/yml file, can't easily determine entrypoint")
×
59
                        }
×
60
                        outDir = filepath.Dir(filePath)
×
61
                }
62

63
                // If the file is in a sub-directory, create it
64
                dir := filepath.Dir(filePath)
×
65
                if _, err := os.Stat(dir); err != nil {
×
66
                        if err := os.MkdirAll(dir, os.ModePerm); err != nil {
×
67
                                return cd, err
×
68
                        }
×
69
                }
70

71
                // Create and write the file
72
                if err := dec.copyTo(f, filePath); err != nil {
×
73
                        return cd, err
×
74
                }
×
75
        }
76

77
        return outDir, nil
×
78
}
79

80
// Based upon https://security.snyk.io/research/zip-slip-vulnerability#expandable-socPI9fFAJ-title
NEW
81
func sanitizeArchivePath(destination, filePath string) (destpath string, err error) {
×
NEW
82
        destpath = filepath.Join(destination, filePath)
×
NEW
83
        if !strings.HasPrefix(destpath, filepath.Clean(destination)+string(os.PathSeparator)) {
×
NEW
84
                return destpath, &ErrPathTainted{
×
NEW
85
                        Path: destination,
×
NEW
86
                }
×
UNCOV
87
        }
×
NEW
88
        return
×
89
}
90

91
func (dec *Decompressor) copyTo(f *zip.File, filePath string) error {
×
92
        outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, f.Mode())
×
93
        if err != nil {
×
94
                return err
×
95
        }
×
96
        defer outFile.Close()
×
97

×
98
        rc, err := f.Open()
×
99
        if err != nil {
×
100
                return err
×
101
        }
×
102
        defer rc.Close()
×
103

×
104
        for {
×
105
                n, err := io.CopyN(outFile, rc, blockSize)
×
106
                if err != nil {
×
107
                        if err == io.EOF {
×
108
                                return nil
×
109
                        }
×
110
                        return err
×
111
                }
112
                dec.currSize += n
×
113

×
114
                if dec.MaxSize > 0 && dec.currSize > dec.MaxSize {
×
115
                        return ErrTooLargeContent{
×
116
                                MaxSize: dec.MaxSize,
×
117
                        }
×
118
                }
×
119
        }
120
}
121

122
// ErrPathTainted is returned when a potential zip slip is detected
123
// through an unzip.
124
type ErrPathTainted struct {
125
        Path string
126
}
127

128
func (err ErrPathTainted) Error() string {
×
129
        return fmt.Sprintf("filepath is tainted: %s", err.Path)
×
130
}
×
131

132
var _ error = (*ErrPathTainted)(nil)
133

134
// ErrTooLargeContent is returned when a too large zip is processed
135
// (e.g. a zip bomb).
136
type ErrTooLargeContent struct {
137
        MaxSize int64
138
}
139

140
func (err ErrTooLargeContent) Error() string {
×
141
        return fmt.Sprintf("too large archive content, maximum is %d", err.MaxSize)
×
142
}
×
143

144
var _ error = (*ErrTooLargeContent)(nil)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc