Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions pkg/commands/subcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func readPackage(dir string) (subcommands, error) {
return subcommands{}, fmt.Errorf("unable to unmarshal package: %v", err)
}

for key := range packageData.Commands {
packageData.Commands[key].Name = strings.ToLower(packageData.Commands[key].Name)
if err := normalizeCommandNames(&packageData); err != nil {
return subcommands{}, err
}

packageData.Pkg = filepath.Base(strings.Replace(dir, "cli-", "", 1))
Expand Down Expand Up @@ -75,8 +75,8 @@ func readPackageFromGithub(url, dir string) (subcommands, error) {

packageData.raw = cliJSON

for key := range packageData.Commands {
packageData.Commands[key].Name = strings.ToLower(packageData.Commands[key].Name)
if err := normalizeCommandNames(&packageData); err != nil {
return subcommands{}, err
}

packageData.Pkg = filepath.Base(strings.Replace(dir, "cli-", "", 1))
Expand Down Expand Up @@ -142,6 +142,11 @@ func downloadBin(ctx context.Context, dir string, cmd command) error {
cmd.BinSuffix = ".exe"
}

commandName, err := tools.NormalizeCommandName(cmd.Name)
if err != nil {
return fmt.Errorf("invalid command name in cli.json: %w", err)
}

t := template.Must(template.New("url").Parse(cmd.Bin))
buf := &bytes.Buffer{}
if err := t.Execute(buf, cmd); err != nil {
Expand All @@ -152,7 +157,7 @@ func downloadBin(ctx context.Context, dir string, cmd command) error {
url := buf.String()
logger.Debug(fmt.Sprintf("Fetching binary from %s", url))

binName := filepath.Join(dir, "akamai-"+strings.ToLower(cmd.Name)+cmd.BinSuffix)
binName := filepath.Join(dir, "akamai-"+commandName+cmd.BinSuffix)
bin, err := os.Create(binName)
if err != nil {
logger.Error(fmt.Sprintf("Unable to create %s file: %v", binName, err))
Expand Down Expand Up @@ -192,3 +197,15 @@ func downloadBin(ctx context.Context, dir string, cmd command) error {

return nil
}

func normalizeCommandNames(packageData *subcommands) error {
for i := range packageData.Commands {
name, err := tools.NormalizeCommandName(packageData.Commands[i].Name)
if err != nil {
return fmt.Errorf("invalid command name in cli.json: %w", err)
}
packageData.Commands[i].Name = name
}

return nil
}
36 changes: 36 additions & 0 deletions pkg/commands/subcommands_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package commands

import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -44,3 +49,34 @@ func TestReadPackage(t *testing.T) {
})
}
}

func TestReadPackageRejectsTraversalCommandName(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "cli.json"), []byte(`{"commands":[{"name":"x/../../escaped"}]}`), 0600))

_, err := readPackage(dir)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid command name in cli.json")
}

func TestReadPackageFromGithubRejectsTraversalCommandName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{"commands":[{"name":"x/../../escaped"}]}`))
require.NoError(t, err)
}))
defer server.Close()

_, err := readPackageFromGithub(server.URL, t.TempDir())
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid command name in cli.json")
}

func TestDownloadBinRejectsTraversalCommandName(t *testing.T) {
dir := t.TempDir()
escapedPath := filepath.Join(filepath.Dir(dir), "escaped")

err := downloadBin(context.Background(), dir, command{Name: "x/../../escaped"})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid command name in cli.json")
assert.NoFileExists(t, escapedPath)
}
17 changes: 15 additions & 2 deletions pkg/packages/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/akamai/cli/v2/pkg/color"
"github.com/akamai/cli/v2/pkg/log"
Expand All @@ -22,6 +21,12 @@ import (
func (l *langManager) installGolang(ctx context.Context, dir, ver string, commands, ldFlags []string) error {
logger := log.FromContext(ctx)

for _, command := range commands {
if _, err := tools.NormalizeCommandName(command); err != nil {
return fmt.Errorf("invalid command name: %w", err)
}
}

goBin, err := l.commandExecutor.LookPath("go")
if err != nil {
logger.Error("Go executable not found")
Expand Down Expand Up @@ -72,7 +77,15 @@ func (l *langManager) installGolang(ctx context.Context, dir, ver string, comman

for n, command := range commands {
ldFlag := ldFlags[n]
execName := "akamai-" + strings.ToLower(command)
commandName, _ := tools.NormalizeCommandName(command)
execName := "akamai-" + commandName
outputPath := filepath.Join(dir, execName)

// A package repository controls its own contents. Remove a pre-existing
// output entry so go build cannot follow a symlink planted in that repo.
if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove existing Go build output %q: %w", outputPath, err)
}
Comment on lines +84 to +88

var cmd *exec.Cmd
params := []string{"build", "-o", execName}
Expand Down
7 changes: 7 additions & 0 deletions pkg/packages/golang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,10 @@ func TestInstallGolang(t *testing.T) {
})
}
}

func TestInstallGolangRejectsTraversalCommandName(t *testing.T) {
l := langManager{new(mocked)}
err := l.installGolang(context.Background(), t.TempDir(), "*", []string{"x/../../escaped"}, []string{""})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid command name")
}
15 changes: 15 additions & 0 deletions pkg/tools/util.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
package tools

import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/urfave/cli/v2"
)

var commandNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`)

// NormalizeCommandName converts a package command name to its canonical form
// and rejects values that cannot safely be used as a command identifier.
func NormalizeCommandName(name string) (string, error) {
normalized := strings.ToLower(name)
if !commandNamePattern.MatchString(normalized) {
return "", fmt.Errorf("%q must match [A-Za-z0-9][A-Za-z0-9_-]*", name)
}

return normalized, nil
}

// Self ...
func Self() string {
return filepath.Base(os.Args[0])
Expand Down
Loading