Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit bdb998e

Browse files
committed
internal/cmd/update.go: check path prefixes
1 parent 6371084 commit bdb998e

File tree

2 files changed

+127
-5
lines changed

2 files changed

+127
-5
lines changed

internal/cmd/update.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414
"net/http"
1515
"net/url"
1616
"os"
17+
"os/exec"
1718
"path"
19+
"path/filepath"
1820
"runtime"
1921
"strings"
2022
"time"
@@ -32,7 +34,8 @@ import (
3234

3335
// updater updates coder-cli.
3436
type updater struct {
35-
confirmF func(label string) (string, error)
37+
confirmF func(string) (string, error)
38+
execF func(context.Context, string, ...string) ([]byte, error)
3639
executablePath string
3740
fs afero.Fs
3841
httpClient getter
@@ -63,6 +66,7 @@ func updateCmd() *cobra.Command {
6366

6467
updater := &updater{
6568
confirmF: defaultConfirm,
69+
execF: defaultExec,
6670
executablePath: currExe,
6771
httpClient: httpClient,
6872
fs: afero.NewOsFs(),
@@ -84,9 +88,27 @@ type getter interface {
8488
}
8589

8690
func (u *updater) Run(ctx context.Context, force bool, coderURLString string) error {
87-
// TODO: check under following directories and warn if coder binary is under them:
91+
// Check under following directories and warn if coder binary is under them:
92+
// * C:\Windows\
8893
// * homebrew prefix
89-
// * coder assets root (env CODER_ASSETS_ROOT)
94+
// * coder assets root (/var/tmp/coder)
95+
var pathBlockList = []string{
96+
`C:\Windows\`,
97+
`/var/tmp/coder`,
98+
}
99+
brewPrefixCmd, err := u.execF(ctx, "brew", "--prefix")
100+
if err == nil { // ignore errors if homebrew not installed
101+
pathBlockList = append(pathBlockList, strings.TrimSpace(string(brewPrefixCmd)))
102+
}
103+
104+
for _, prefix := range pathBlockList {
105+
if HasFilePathPrefix(u.executablePath, prefix) {
106+
return clog.Fatal(
107+
"cowardly refusing to update coder binary",
108+
clog.BlankLine,
109+
clog.Causef("executable path %q is under blocklisted prefix %q", u.executablePath, prefix))
110+
}
111+
}
90112

91113
currentBinaryStat, err := u.fs.Stat(u.executablePath)
92114
if err != nil {
@@ -312,7 +334,7 @@ func getCoderConfigURL() (*url.URL, error) {
312334
}
313335

314336
// XXX: coder.Client requires an API key, but we may not be logged into the coder instance for which we
315-
// want to determine the version. We don't need an API key to sniff the version header.
337+
// want to determine the version. We don't need an API key to hit /api/private/version though.
316338
func getAPIVersionUnauthed(client getter, baseURL url.URL) (*semver.Version, error) {
317339
baseURL.Path = path.Join(baseURL.Path, "/api/private/version")
318340
resp, err := client.Get(baseURL.String())
@@ -341,3 +363,33 @@ func getAPIVersionUnauthed(client getter, baseURL url.URL) (*semver.Version, err
341363

342364
return version, nil
343365
}
366+
367+
// HasFilePathPrefix reports whether the filesystem path s
368+
// begins with the elements in prefix.
369+
// Lifted from github.com/golang/go/blob/master/src/cmd/internal/str/path.go
370+
func HasFilePathPrefix(s, prefix string) bool {
371+
sv := strings.ToUpper(filepath.VolumeName(s))
372+
pv := strings.ToUpper(filepath.VolumeName(prefix))
373+
s = s[len(sv):]
374+
prefix = prefix[len(pv):]
375+
switch {
376+
default:
377+
return false
378+
case sv != pv:
379+
return false
380+
case len(s) == len(prefix):
381+
return s == prefix
382+
case prefix == "":
383+
return true
384+
case len(s) > len(prefix):
385+
if prefix[len(prefix)-1] == filepath.Separator {
386+
return strings.HasPrefix(s, prefix)
387+
}
388+
return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
389+
}
390+
}
391+
392+
// defaultExec wraps exec.CommandContext
393+
func defaultExec(ctx context.Context, cmd string, args ...string) ([]byte, error) {
394+
return exec.CommandContext(ctx, cmd, args...).CombinedOutput()
395+
}

internal/cmd/update_test.go

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http"
1212
"os"
1313
"path/filepath"
14+
"runtime"
1415
"strings"
1516
"testing"
1617

@@ -32,7 +33,6 @@ const (
3233
var (
3334
apiPrivateVersionURL = fakeCoderURL + "/api/private/version"
3435
fakeNewVersionJson = fmt.Sprintf(`{"version":%q}`, fakeNewVersion)
35-
fakeOldVersionJson = fmt.Sprintf(`{"version":%q}`, fakeOldVersion)
3636
)
3737

3838
func Test_updater_run(t *testing.T) {
@@ -42,6 +42,7 @@ func Test_updater_run(t *testing.T) {
4242
type params struct {
4343
ConfirmF func(string) (string, error)
4444
Ctx context.Context
45+
Execer *fakeExecer
4546
ExecutablePath string
4647
Fakefs afero.Fs
4748
HttpClient *fakeGetter
@@ -53,6 +54,7 @@ func Test_updater_run(t *testing.T) {
5354
fromParams := func(p *params) *updater {
5455
return &updater{
5556
confirmF: p.ConfirmF,
57+
execF: p.Execer.ExecF,
5658
executablePath: p.ExecutablePath,
5759
fs: p.Fakefs,
5860
httpClient: p.HttpClient,
@@ -65,13 +67,16 @@ func Test_updater_run(t *testing.T) {
6567
t.Logf("running %s", name)
6668
ctx := context.Background()
6769
fakefs := afero.NewMemMapFs()
70+
execer := newFakeExecer(t)
71+
execer.M["brew --prefix"] = fakeExecerResult{[]byte{}, os.ErrNotExist}
6872
params := &params{
6973
// This must be overridden inside run()
7074
ConfirmF: func(string) (string, error) {
7175
t.Errorf("unhandled ConfirmF")
7276
t.FailNow()
7377
return "", nil
7478
},
79+
Execer: execer,
7580
Ctx: ctx,
7681
ExecutablePath: fakeExePathLinux,
7782
Fakefs: fakefs,
@@ -270,6 +275,43 @@ func Test_updater_run(t *testing.T) {
270275
assert.ErrorContains(t, "update coder - read-only fs", err, "failed to create file")
271276
assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion)
272277
})
278+
279+
if runtime.GOOS == "windows" {
280+
run(t, "update coder - path blocklist - windows", func(t *testing.T, p *params) {
281+
p.ExecutablePath = `C:\Windows\system32\coder.exe`
282+
u := fromParams(p)
283+
err := u.Run(p.Ctx, false, fakeCoderURL)
284+
assert.ErrorContains(t, "update coder - path blocklist - windows", err, "cowardly refusing to update coder binary")
285+
})
286+
} else {
287+
run(t, "update coder - path blocklist - coder assets dir", func(t *testing.T, p *params) {
288+
p.ExecutablePath = `/var/tmp/coder/coder`
289+
u := fromParams(p)
290+
err := u.Run(p.Ctx, false, fakeCoderURL)
291+
assert.ErrorContains(t, "update coder - path blocklist - windows", err, "cowardly refusing to update coder binary")
292+
})
293+
run(t, "update coder - path blocklist - old homebrew prefix", func(t *testing.T, p *params) {
294+
p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/usr/local"), nil}
295+
p.ExecutablePath = `/usr/local/bin/coder`
296+
u := fromParams(p)
297+
err := u.Run(p.Ctx, false, fakeCoderURL)
298+
assert.ErrorContains(t, "update coder - path blocklist - old homebrew prefix", err, "cowardly refusing to update coder binary")
299+
})
300+
run(t, "update coder - path blocklist - new homebrew prefix", func(t *testing.T, p *params) {
301+
p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/opt/homebrew"), nil}
302+
p.ExecutablePath = `/opt/homebrew/bin/coder`
303+
u := fromParams(p)
304+
err := u.Run(p.Ctx, false, fakeCoderURL)
305+
assert.ErrorContains(t, "update coder - path blocklist - new homebrew prefix", err, "cowardly refusing to update coder binary")
306+
})
307+
run(t, "update coder - path blocklist - linuxbrew", func(t *testing.T, p *params) {
308+
p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/home/user/.linuxbrew"), nil}
309+
p.ExecutablePath = `/home/user/.linuxbrew/bin/coder`
310+
u := fromParams(p)
311+
err := u.Run(p.Ctx, false, fakeCoderURL)
312+
assert.ErrorContains(t, "update coder - path blocklist - linuxbrew", err, "cowardly refusing to update coder binary")
313+
})
314+
}
273315
}
274316

275317
// fakeGetter mocks HTTP requests
@@ -378,3 +420,31 @@ var fakeValidZipBytes, _ = base64.StdEncoding.DecodeString(`UEsDBAoAAAAAAAtfDVNC
378420
BOgDAAAE6AMAADEuMjMuNC1yYy41KzY3OC1nYWJjZGVmLTEyMzQ1Njc4UEsBAh4DCgAAAAAAC18N
379421
U0Ic0MIgAAAAIAAAAAkAGAAAAAAAAQAAAO2BAAAAAGNvZGVyLmV4ZVVUBQAD5l0WYXV4CwABBOgD
380422
AAAE6AMAAFBLBQYAAAAAAQABAE8AAABjAAAAAAA=`)
423+
424+
type fakeExecer struct {
425+
M map[string]fakeExecerResult
426+
T *testing.T
427+
}
428+
429+
func (f *fakeExecer) ExecF(_ context.Context, cmd string, args ...string) ([]byte, error) {
430+
cmdAndArgs := strings.Join(append([]string{cmd}, args...), " ")
431+
val, ok := f.M[cmdAndArgs]
432+
if !ok {
433+
f.T.Errorf("unhandled cmd %q", cmd)
434+
f.T.FailNow()
435+
return nil, nil // will never happen
436+
}
437+
return val.Output, val.Err
438+
}
439+
440+
func newFakeExecer(t *testing.T) *fakeExecer {
441+
return &fakeExecer{
442+
M: make(map[string]fakeExecerResult),
443+
T: t,
444+
}
445+
}
446+
447+
type fakeExecerResult struct {
448+
Output []byte
449+
Err error
450+
}

0 commit comments

Comments
 (0)