Skip to content

Commit 38e9738

Browse files
nywilkenKent 'picat' Grubersylviamoss
authored
Multiple fixes for go-getter v2 (#361)
* Fix command injection in go-getter when passing params to hg clone The fix for this is to add -- to the arguments of each hg command, before any user-input. This indicates the end of optional arguments, only positional arguments are allowed. * Remove upwards path traversal in subdirectories, filenames * Prevent arbitrary file read, path traversal via subdirectory extraction Not opt-in or opt-out, just never allowed. Upwards path traversal is not a subdirectory. *Prevent arbitrary file write via `filename` Not opt-in or opt-out, just never allowed. Upwards path traversal is not a filename in a subdirectory. * Add Timeout option to HgGetter and GitGetter enforced with os/exec.CommandContext * Add DisableSymlinks option to getter request The fix for this is a new client request option, DisableSymlinks. When set to true, symlinks are disabled. This prevents the client, likely in combination with the GitGetter, from following a symlink when the subdirectory selection from the checked out repo is a symlink. * Add custom symlink copy error * Add DisableSymlinks as client option Setting DisableSymlinks per request works but must be set on all request made by a client. Adding it as a top-level client config option allows for setting DisableSymlinks for all client.Get requests. * Update get_http to address various get concerns * Add XTerraformGetLimit and XTerraformGetDisabled * Add Multiple new options to limit resource consumption: DoNotCheckHeadFirst, HeadFirstTimeout, ReadTimeout, MaxBytes * Add getter client to context for reuse * Add setters/getters for storing configured getter.Client in a context * Update HttpGetter to use ClientFromContext when available; otherwise use a limited client for supporting X-Terraform-Get request * Refactor HttpGetter function to make it clear when a configured getter.Client is required * Add security section to README * Port changes from hashicorp/eastebry/timeout-for-getters Adding timeout to s3Getter * Port changes from from hashicorp/add-missing-timeouts Add missing timeouts to `S3Getter` and `GCSGetter` * Remove windows test for FileGetter * Change to next-get image Co-authored-by: Kent 'picat' Gruber <[email protected]> Co-authored-by: Sylvia Moss <[email protected]>
1 parent 4e45866 commit 38e9738

21 files changed

+1470
-164
lines changed

.circleci/config.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ commands:
4949
jobs:
5050
linux-tests:
5151
docker:
52-
- image: circleci/golang:<< parameters.go-version >>
52+
- image: cimg/go:<< parameters.go-version >>
5353
parameters:
5454
go-version:
5555
type: string
56-
environment:
56+
environment:
5757
<<: *ENVIRONMENT
5858
parallelism: 4
5959
steps:
@@ -104,7 +104,7 @@ jobs:
104104
path: *TEST_RESULTS_PATH
105105

106106
windows-tests:
107-
executor:
107+
executor:
108108
name: win/default
109109
shell: bash --login -eo pipefail
110110
environment:
@@ -115,12 +115,12 @@ jobs:
115115
type: string
116116
gotestsum-version:
117117
type: string
118-
steps:
118+
steps:
119119
- run: git config --global core.autocrlf false
120120
- checkout
121121
- attach_workspace:
122122
at: .
123-
- run:
123+
- run:
124124
name: Setup (remove pre-installed go)
125125
command: |
126126
rm -rf "c:\Go"
@@ -131,16 +131,16 @@ jobs:
131131
- win-golang-<< parameters.go-version >>-cache-v1
132132
- win-gomod-cache-{{ checksum "go.mod" }}-v1
133133

134-
- run:
134+
- run:
135135
name: Install go version << parameters.go-version >>
136-
command: |
136+
command: |
137137
if [ ! -d "c:\go" ]; then
138138
echo "Cache not found, installing new version of go"
139139
curl --fail --location https://dl.google.com/go/go<< parameters.go-version >>.windows-amd64.zip --output go.zip
140140
unzip go.zip -d "/c"
141141
fi
142142
143-
- run:
143+
- run:
144144
command: go mod download
145145

146146
- save_cache:
@@ -176,7 +176,7 @@ jobs:
176176

177177
go-smb-test:
178178
docker:
179-
- image: circleci/golang:<< parameters.go-version >>
179+
- image: cimg/go:<< parameters.go-version >>
180180
parameters:
181181
go-version:
182182
type: string

README.md

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ URLs. For example: "github.com/hashicorp/go-getter" would turn into a
2121
Git URL. Or "./foo" would turn into a file URL. These are extensible.
2222

2323
This library is used by [Terraform](https://terraform.io) for
24-
downloading modules and [Nomad](https://nomadproject.io) for downloading
25-
binaries.
24+
downloading modules, [Packer](https://packer.io) for downloading binaries, and
25+
[Nomad](https://nomadproject.io) for downloading binaries.
2626

2727
## Installation and Usage
2828

@@ -47,6 +47,16 @@ $ go-getter github.com/foo/bar ./foo
4747

4848
The command is useful for verifying URL structures.
4949

50+
## Security
51+
Fetching resources from user-supplied URLs is an inherently dangerous operation and may
52+
leave your application vulnerable to [server side request forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery),
53+
[path traversal](https://owasp.org/www-community/attacks/Path_Traversal), [denial of service](https://owasp.org/www-community/attacks/Denial_of_Service)
54+
or other security flaws.
55+
56+
go-getter contains mitigations for some of these security issues, but should still be used with
57+
caution in security-critical contexts. See the available [security options](#Security-Options) that
58+
can be configured to mitigate some of these risks.
59+
5060
## URL Format
5161

5262
go-getter uses a single string URL as input to download from a variety of
@@ -83,7 +93,7 @@ is built-in by default:
8393
file URLs.
8494
* GitHub URLs, such as "github.com/mitchellh/vagrant" are automatically
8595
changed to Git protocol over HTTP.
86-
* GitLab URLs, such as "gitlab.com/inkscape/inkscape" are automatically
96+
* GitLab URLs, such as "gitlab.com/inkscape/inkscape" are automatically
8797
changed to Git protocol over HTTP.
8898
* BitBucket URLs, such as "bitbucket.org/mitchellh/vagrant" are automatically
8999
changed to a Git or mercurial protocol using the BitBucket API.
@@ -178,7 +188,7 @@ checksum string. Examples:
178188
```
179189
./foo.txt?checksum=file:./foo.txt.sha256sum
180190
```
181-
191+
182192
When checksumming from a file - ex: with `checksum=file:url` - go-getter will
183193
get the file linked in the URL after `file:` using the same configuration. For
184194
example, in `file:http://releases.ubuntu.com/cosmic/MD5SUMS` go-getter will
@@ -279,7 +289,7 @@ None
279289
from a private key file on disk, you would run `base64 -w0 <file>`.
280290

281291
**Note**: Git 2.3+ is required to use this feature.
282-
292+
283293
* `depth` - The Git clone depth. The provided number specifies the last `n`
284294
revisions to clone from the repository.
285295

@@ -374,35 +384,107 @@ files from a smb shared folder whenever the url is prefixed with `smb://`.
374384

375385
⚠️ The [`smbclient`](https://www.samba.org/samba/docs/current/man-html/smbclient.1.html) command is available only for Linux.
376386
This is the ONLY option for a Linux user and therefore the client must be installed.
377-
387+
378388
The `smbclient` cli is not available for Windows and MacOS. The go-getter
379389
will try to get files using the file system, when this happens the getter uses the FileGetter implementation.
380390

381-
When connecting to a smb server, the OS creates a local mount in a system specific volume folder, and go-getter will
391+
When connecting to a smb server, the OS creates a local mount in a system specific volume folder, and go-getter will
382392
try to access the following folders when looking for local mounts.
383393

384394
- MacOS: /Volumes/<shared_path>
385395
- Windows: \\\\\<host>\\\<shared_path>
386396

387-
The following examples work for all the OSes:
397+
The following examples work for all the OSes:
388398
- smb://host/shared/dir (downloads directory content)
389-
- smb://host/shared/dir/file (downloads file)
399+
- smb://host/shared/dir/file (downloads file)
390400

391-
The following examples work for Linux:
401+
The following examples work for Linux:
392402
- smb://username:password@host/shared/dir (downloads directory content)
393403
- smb://username@host/shared/dir
394404
- smb://username:password@host/shared/dir/file (downloads file)
395405
- smb://username@host/shared/dir/file
396406

397407
⚠️ The above examples also work on the other OSes but the authentication is not used to access the file system.
398408

399-
400-
409+
410+
401411
#### SMB Testing
402412
The test for `get_smb.go` requires a smb server running which can be started inside a docker container by
403-
running `make start-smb`. Once the container is up the shared folder can be accessed via `smb://<ip|name>/public/<dir|file>` or
404-
`smb://user:password@<ip|name>/private/<dir|file>` by another container or machine in the same network.
413+
running `make start-smb`. Once the container is up the shared folder can be accessed via `smb://<ip|name>/public/<dir|file>` or
414+
`smb://user:password@<ip|name>/private/<dir|file>` by another container or machine in the same network.
405415

406-
To run the tests inside `get_smb_test.go` and `client_test.go`, prepare the environment with `make smbtests-prepare`. On prepare some
416+
To run the tests inside `get_smb_test.go` and `client_test.go`, prepare the environment with `make smbtests-prepare`. On prepare some
407417
mock files and directories will be added to the shared folder and a go-getter container will start together with the samba server.
408-
Once the environment for testing is prepared, run `make smbtests` to run the tests.
418+
Once the environment for testing is prepared, run `make smbtests` to run the tests.
419+
420+
### Security Options
421+
422+
**Disable Symlinks**
423+
424+
In your getter client config, we recommend using the `DisableSymlinks` option,
425+
which prevents writing through or copying from symlinks (which may point outside the directory).
426+
427+
```go
428+
client := getter.Client{
429+
// This will prevent copying or writing files through symlinks
430+
DisableSymlinks: true,
431+
}
432+
```
433+
434+
**Disable or Limit `X-Terraform-Get`**
435+
436+
Go-Getter supports arbitrary redirects via the `X-Terraform-Get` header. This functionality
437+
exists to support [Terraform use cases](https://www.terraform.io/language/modules/sources#http-urls),
438+
but is likely not needed in most applications.
439+
440+
For code that uses the `HttpGetter`, add the following configuration options:
441+
442+
```go
443+
var httpGetter = &getter.HttpGetter{
444+
// Most clients should disable X-Terraform-Get
445+
// See the note below
446+
XTerraformGetDisabled: true,
447+
// Your software probably doesn’t rely on X-Terraform-Get, but
448+
// if it does, you should set the above field to false, plus
449+
// set XTerraformGet Limit to prevent endless redirects
450+
// XTerraformGetLimit: 10,
451+
}
452+
```
453+
454+
**Enforce Timeouts**
455+
456+
The `HttpGetter` supports timeouts and other resource-constraining configuration options. The `GitGetter` and `HgGetter`
457+
only support timeouts.
458+
459+
Configuration for the `HttpGetter`:
460+
461+
```go
462+
var httpGetter = &getter.HttpGetter{
463+
// Disable pre-fetch HEAD requests
464+
DoNotCheckHeadFirst: true,
465+
466+
// As an alternative to the above setting, you can
467+
// set a reasonable timeout for HEAD requests
468+
// HeadFirstTimeout: 10 * time.Second,
469+
// Read timeout for HTTP operations
470+
ReadTimeout: 30 * time.Second,
471+
// Set the maximum number of bytes
472+
// that can be read by the getter
473+
MaxBytes: 500000000, // 500 MB
474+
}
475+
```
476+
477+
For code that uses the `GitGetter` or `HgGetter`, set the `Timeout` option:
478+
```go
479+
var gitGetter = &getter.GitGetter{
480+
// Set a reasonable timeout for git operations
481+
Timeout: 5 * time.Minute,
482+
}
483+
```
484+
485+
```go
486+
var hgGetter = &getter.HgGetter{
487+
// Set a reasonable timeout for hg operations
488+
Timeout: 5 * time.Minute,
489+
}
490+
```

client.go

Lines changed: 35 additions & 2 deletions
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"
@@ -14,6 +15,9 @@ import (
1415
safetemp "github.com/hashicorp/go-safetemp"
1516
)
1617

18+
// ErrSymlinkCopy means that a copy of a symlink was encountered on a request with DisableSymlinks enabled.
19+
var ErrSymlinkCopy = errors.New("copying of symlinks has been disabled")
20+
1721
// Client is a client for downloading things.
1822
//
1923
// Top-level functions such as Get are shortcuts for interacting with a client.
@@ -27,6 +31,10 @@ type Client struct {
2731
// Getters is the list of protocols supported by this client. If this
2832
// is nil, then the default Getters variable will be used.
2933
Getters []Getter
34+
35+
// Disable symlinks is used to prevent copying or writing files through symlinks for Get requests.
36+
// When set to true any copying or writing through symlinks will result in a ErrSymlinkCopy error.
37+
DisableSymlinks bool
3038
}
3139

3240
// GetResult is the result of a Client.Get
@@ -41,15 +49,36 @@ func (c *Client) Get(ctx context.Context, req *Request) (*GetResult, error) {
4149
return nil, err
4250
}
4351

52+
// Pass along the configured Getter client in the context for usage with the X-Terraform-Get feature.
53+
ctx = NewContextWithClient(ctx, c)
54+
4455
// Store this locally since there are cases we swap this
4556
if req.GetMode == ModeInvalid {
4657
req.GetMode = ModeAny
4758
}
4859

60+
// Client setting takes precedence for all requests
61+
if c.DisableSymlinks {
62+
req.DisableSymlinks = true
63+
}
64+
4965
// If there is a subdir component, then we download the root separately
5066
// and then copy over the proper subdir.
5167
req.Src, req.subDir = SourceDirSubdir(req.Src)
68+
5269
if req.subDir != "" {
70+
// Check if the subdirectory is attempting to traverse upwards, outside of
71+
// the cloned repository path.
72+
req.subDir = filepath.Clean(req.subDir)
73+
if containsDotDot(req.subDir) {
74+
return nil, fmt.Errorf("subdirectory component contain path traversal out of the repository")
75+
}
76+
77+
// Prevent absolute paths, remove a leading path separator from the subdirectory
78+
if req.subDir[0] == os.PathSeparator {
79+
req.subDir = req.subDir[1:]
80+
}
81+
5382
td, tdcloser, err := safetemp.Dir("", "getter")
5483
if err != nil {
5584
return nil, err
@@ -123,7 +152,7 @@ func (c *Client) get(ctx context.Context, req *Request, g Getter) (*GetResult, *
123152
// Determine if we have an archive type
124153
archiveV := q.Get("archive")
125154
if archiveV != "" {
126-
// Delete the paramter since it is a magic parameter we don't
155+
// Delete the parameter since it is a magic parameter we don't
127156
// want to pass on to the Getter
128157
q.Del("archive")
129158
req.u.RawQuery = q.Encode()
@@ -199,6 +228,10 @@ func (c *Client) get(ctx context.Context, req *Request, g Getter) (*GetResult, *
199228
filename = v
200229
}
201230

231+
if containsDotDot(filename) {
232+
return nil, &getError{true, fmt.Errorf("filename query parameter contain path traversal")}
233+
}
234+
202235
req.Dst = filepath.Join(req.Dst, filename)
203236
}
204237
}
@@ -284,7 +317,7 @@ func (c *Client) get(ctx context.Context, req *Request, g Getter) (*GetResult, *
284317
return nil, &getError{true, err}
285318
}
286319

287-
err = copyDir(ctx, req.realDst, subDir, false, req.umask())
320+
err = copyDir(ctx, req.realDst, subDir, false, req.DisableSymlinks, req.umask())
288321
if err != nil {
289322
return nil, &getError{false, err}
290323
}

client_option.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
package getter
22

3+
import (
4+
"context"
5+
)
6+
7+
type clientContextKey int
8+
9+
const clientContextValue clientContextKey = 0
10+
11+
func NewContextWithClient(ctx context.Context, client *Client) context.Context {
12+
return context.WithValue(ctx, clientContextValue, client)
13+
}
14+
15+
func ClientFromContext(ctx context.Context) *Client {
16+
// ctx.Value returns nil if ctx has no value for the key;
17+
client, ok := ctx.Value(clientContextValue).(*Client)
18+
if !ok {
19+
return nil
20+
}
21+
return client
22+
}
23+
324
// configure configures a client with options.
425
func (c *Client) configure() error {
526
// Default decompressor values

cmd/go-getter/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
func main() {
1717
modeRaw := flag.String("mode", "any", "get mode (any, file, dir)")
1818
progress := flag.Bool("progress", false, "display terminal progress")
19+
noSymlinks := flag.Bool("disable-symlinks", false, "prevent copying or writing files through symlinks")
1920
flag.Parse()
2021
args := flag.Args()
2122
if len(args) < 2 {
@@ -54,12 +55,16 @@ func main() {
5455
if *progress {
5556
req.ProgressListener = defaultProgressBar
5657
}
57-
5858
wg := sync.WaitGroup{}
5959
wg.Add(1)
6060

6161
client := getter.DefaultClient
6262

63+
// Disable symlinks for all client requests
64+
if *noSymlinks {
65+
client.DisableSymlinks = true
66+
}
67+
6368
getters := getter.Getters
6469
getters = append(getters, new(gcs.Getter))
6570
getters = append(getters, new(s3.Getter))

0 commit comments

Comments
 (0)