Skip to content

Commit a2ebce9

Browse files
eastebryKent 'picat' Gruber
andauthored
Multiple fixes for go-getter (#359)
* Multiple fixes for go-getter Co-authored-by: Kent 'picat' Gruber <[email protected]>
1 parent 4553965 commit a2ebce9

16 files changed

+1169
-97
lines changed

client.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package getter
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io/ioutil"
78
"os"
@@ -13,6 +14,9 @@ import (
1314
safetemp "github.com/hashicorp/go-safetemp"
1415
)
1516

17+
// ErrSymlinkCopy means that a copy of a symlink was encountered on a request with DisableSymlinks enabled.
18+
var ErrSymlinkCopy = errors.New("copying of symlinks has been disabled")
19+
1620
// Client is a client for downloading things.
1721
//
1822
// Top-level functions such as Get are shortcuts for interacting with a client.
@@ -76,6 +80,9 @@ type Client struct {
7680
// This is identical to tls.Config.InsecureSkipVerify.
7781
Insecure bool
7882

83+
// Disable symlinks
84+
DisableSymlinks bool
85+
7986
Options []ClientOption
8087
}
8188

@@ -123,6 +130,17 @@ func (c *Client) Get() error {
123130
dst := c.Dst
124131
src, subDir := SourceDirSubdir(src)
125132
if subDir != "" {
133+
// Check if the subdirectory is attempting to traverse updwards, outside of
134+
// the cloned repository path.
135+
subDir := filepath.Clean(subDir)
136+
if containsDotDot(subDir) {
137+
return fmt.Errorf("subdirectory component contain path traversal out of the repository")
138+
}
139+
// Prevent absolute paths, remove a leading path separator from the subdirectory
140+
if subDir[0] == os.PathSeparator {
141+
subDir = subDir[1:]
142+
}
143+
126144
td, tdcloser, err := safetemp.Dir("", "getter")
127145
if err != nil {
128146
return err
@@ -230,6 +248,10 @@ func (c *Client) Get() error {
230248
filename = v
231249
}
232250

251+
if containsDotDot(filename) {
252+
return fmt.Errorf("filename query parameter contain path traversal")
253+
}
254+
233255
dst = filepath.Join(dst, filename)
234256
}
235257
}
@@ -318,7 +340,7 @@ func (c *Client) Get() error {
318340
return err
319341
}
320342

321-
return copyDir(c.Ctx, realDst, subDir, false, c.umask())
343+
return copyDir(c.Ctx, realDst, subDir, false, c.DisableSymlinks, c.umask())
322344
}
323345

324346
return nil

client_option.go

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,100 @@
11
package getter
22

3-
import "context"
3+
import (
4+
"context"
5+
"os"
6+
)
47

5-
// A ClientOption allows to configure a client
8+
// ClientOption is used to configure a client.
69
type ClientOption func(*Client) error
710

8-
// Configure configures a client with options.
11+
// Configure applies all of the given client options, along with any default
12+
// behavior including context, decompressors, detectors, and getters used by
13+
// the client.
914
func (c *Client) Configure(opts ...ClientOption) error {
15+
// If the context has not been configured use the background context.
1016
if c.Ctx == nil {
1117
c.Ctx = context.Background()
1218
}
19+
20+
// Store the options used to configure this client.
1321
c.Options = opts
22+
23+
// Apply all of the client options.
1424
for _, opt := range opts {
1525
err := opt(c)
1626
if err != nil {
1727
return err
1828
}
1929
}
20-
// Default decompressor values
30+
31+
// If the client was not configured with any Decompressors, Detectors,
32+
// or Getters, use the default values for each.
2133
if c.Decompressors == nil {
2234
c.Decompressors = Decompressors
2335
}
24-
// Default detector values
2536
if c.Detectors == nil {
2637
c.Detectors = Detectors
2738
}
28-
// Default getter values
2939
if c.Getters == nil {
3040
c.Getters = Getters
3141
}
3242

43+
// Set the client for each getter, so the top-level client can know
44+
// the getter-specific client functions or progress tracking.
3345
for _, getter := range c.Getters {
3446
getter.SetClient(c)
3547
}
48+
3649
return nil
3750
}
3851

3952
// WithContext allows to pass a context to operation
4053
// in order to be able to cancel a download in progress.
41-
func WithContext(ctx context.Context) func(*Client) error {
54+
func WithContext(ctx context.Context) ClientOption {
4255
return func(c *Client) error {
4356
c.Ctx = ctx
4457
return nil
4558
}
4659
}
60+
61+
// WithDecompressors specifies which Decompressor are available.
62+
func WithDecompressors(decompressors map[string]Decompressor) ClientOption {
63+
return func(c *Client) error {
64+
c.Decompressors = decompressors
65+
return nil
66+
}
67+
}
68+
69+
// WithDecompressors specifies which compressors are available.
70+
func WithDetectors(detectors []Detector) ClientOption {
71+
return func(c *Client) error {
72+
c.Detectors = detectors
73+
return nil
74+
}
75+
}
76+
77+
// WithGetters specifies which getters are available.
78+
func WithGetters(getters map[string]Getter) ClientOption {
79+
return func(c *Client) error {
80+
c.Getters = getters
81+
return nil
82+
}
83+
}
84+
85+
// WithMode specifies which client mode the getters should operate in.
86+
func WithMode(mode ClientMode) ClientOption {
87+
return func(c *Client) error {
88+
c.Mode = mode
89+
return nil
90+
}
91+
}
92+
93+
// WithUmask specifies how to mask file permissions when storing local
94+
// files or decompressing an archive.
95+
func WithUmask(mode os.FileMode) ClientOption {
96+
return func(c *Client) error {
97+
c.Umask = mode
98+
return nil
99+
}
100+
}

copy_dir.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package getter
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"strings"
@@ -16,8 +17,11 @@ func mode(mode, umask os.FileMode) os.FileMode {
1617
// should already exist.
1718
//
1819
// If ignoreDot is set to true, then dot-prefixed files/folders are ignored.
19-
func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, umask os.FileMode) error {
20-
src, err := filepath.EvalSymlinks(src)
20+
func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, disableSymlinks bool, umask os.FileMode) error {
21+
// We can safely evaluate the symlinks here, even if disabled, because they
22+
// will be checked before actual use in walkFn and copyFile
23+
var err error
24+
src, err = filepath.EvalSymlinks(src)
2125
if err != nil {
2226
return err
2327
}
@@ -26,6 +30,20 @@ func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, umask
2630
if err != nil {
2731
return err
2832
}
33+
34+
if disableSymlinks {
35+
fileInfo, err := os.Lstat(path)
36+
if err != nil {
37+
return fmt.Errorf("failed to check copy file source for symlinks: %w", err)
38+
}
39+
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
40+
return ErrSymlinkCopy
41+
}
42+
// if info.Mode()&os.ModeSymlink == os.ModeSymlink {
43+
// return ErrSymlinkCopy
44+
// }
45+
}
46+
2947
if path == src {
3048
return nil
3149
}
@@ -59,7 +77,7 @@ func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, umask
5977
}
6078

6179
// If we have a file, copy the contents.
62-
_, err = copyFile(ctx, dstPath, path, info.Mode(), umask)
80+
_, err = copyFile(ctx, dstPath, path, disableSymlinks, info.Mode(), umask)
6381
return err
6482
}
6583

get_file_copy.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package getter
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67
"os"
78
)
@@ -49,7 +50,17 @@ func copyReader(dst string, src io.Reader, fmode, umask os.FileMode) error {
4950
}
5051

5152
// copyFile copies a file in chunks from src path to dst path, using umask to create the dst file
52-
func copyFile(ctx context.Context, dst, src string, fmode, umask os.FileMode) (int64, error) {
53+
func copyFile(ctx context.Context, dst, src string, disableSymlinks bool, fmode, umask os.FileMode) (int64, error) {
54+
if disableSymlinks {
55+
fileInfo, err := os.Lstat(src)
56+
if err != nil {
57+
return 0, fmt.Errorf("failed to check copy file source for symlinks: %w", err)
58+
}
59+
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
60+
return 0, ErrSymlinkCopy
61+
}
62+
}
63+
5364
srcF, err := os.Open(src)
5465
if err != nil {
5566
return 0, err

get_file_unix.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ func (g *FileGetter) GetFile(dst string, u *url.URL) error {
8787
return os.Symlink(path, dst)
8888
}
8989

90+
var disableSymlinks bool
91+
92+
if g.client != nil && g.client.DisableSymlinks {
93+
disableSymlinks = true
94+
}
95+
9096
// Copy
91-
_, err = copyFile(ctx, dst, path, fi.Mode(), g.client.umask())
97+
_, err = copyFile(ctx, dst, path, disableSymlinks, fi.Mode(), g.client.umask())
9298
return err
9399
}

get_file_windows.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,14 @@ func (g *FileGetter) GetFile(dst string, u *url.URL) error {
111111
}
112112
}
113113

114+
var disableSymlinks bool
115+
116+
if g.client != nil && g.client.DisableSymlinks {
117+
disableSymlinks = true
118+
}
119+
114120
// Copy
115-
_, err = copyFile(ctx, dst, path, 0666, g.client.umask())
121+
_, err = copyFile(ctx, dst, path, disableSymlinks, 0666, g.client.umask())
116122
return err
117123
}
118124

get_gcs.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package getter
33
import (
44
"context"
55
"fmt"
6-
"golang.org/x/oauth2"
7-
"google.golang.org/api/option"
86
"net/url"
97
"os"
108
"path/filepath"
119
"strconv"
1210
"strings"
11+
"time"
12+
13+
"golang.org/x/oauth2"
14+
"google.golang.org/api/option"
1315

1416
"cloud.google.com/go/storage"
1517
"google.golang.org/api/iterator"
@@ -19,11 +21,21 @@ import (
1921
// a GCS bucket.
2022
type GCSGetter struct {
2123
getter
24+
25+
// Timeout sets a deadline which all GCS operations should
26+
// complete within. Zero value means no timeout.
27+
Timeout time.Duration
2228
}
2329

2430
func (g *GCSGetter) ClientMode(u *url.URL) (ClientMode, error) {
2531
ctx := g.Context()
2632

33+
if g.Timeout > 0 {
34+
var cancel context.CancelFunc
35+
ctx, cancel = context.WithTimeout(ctx, g.Timeout)
36+
defer cancel()
37+
}
38+
2739
// Parse URL
2840
bucket, object, _, err := g.parseURL(u)
2941
if err != nil {
@@ -61,6 +73,12 @@ func (g *GCSGetter) ClientMode(u *url.URL) (ClientMode, error) {
6173
func (g *GCSGetter) Get(dst string, u *url.URL) error {
6274
ctx := g.Context()
6375

76+
if g.Timeout > 0 {
77+
var cancel context.CancelFunc
78+
ctx, cancel = context.WithTimeout(ctx, g.Timeout)
79+
defer cancel()
80+
}
81+
6482
// Parse URL
6583
bucket, object, _, err := g.parseURL(u)
6684
if err != nil {
@@ -120,6 +138,12 @@ func (g *GCSGetter) Get(dst string, u *url.URL) error {
120138
func (g *GCSGetter) GetFile(dst string, u *url.URL) error {
121139
ctx := g.Context()
122140

141+
if g.Timeout > 0 {
142+
var cancel context.CancelFunc
143+
ctx, cancel = context.WithTimeout(ctx, g.Timeout)
144+
defer cancel()
145+
}
146+
123147
// Parse URL
124148
bucket, object, fragment, err := g.parseURL(u)
125149
if err != nil {

0 commit comments

Comments
 (0)