Skip to content

Commit 92c06b4

Browse files
Namanclaude
authored andcommitted
fix: resolve 3 CRITICAL panics + 3 HIGH security vulnerabilities
CRITICAL fixes: - Guard all `d.ID[:8]` with length checks to prevent panic on short IDs (cmd/deployments/helpers.go, cmd/open/open.go) - Guard `(*d.EnvironmentID)[:8]` with length check (cmd/domains/domains_list.go) - Fix PaginatedResponse.Pagination nesting — was always zero-valued because Pagination was a sibling of Data instead of nested inside it. Skills catalog TUI pagination (total count, hasNextPage) was silently broken. Security fixes: - Validate --api-url is HTTPS to prevent credential theft via attacker-controlled server (localhost/127.0.0.1 allowed for development) - Reject absolute paths and '..' traversal in env pull/push --file flag to prevent writing to or reading from arbitrary filesystem locations - Sanitize template name with filepath.Base() when used as directory name to prevent path traversal via malicious template names Closes #14 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 93ffe46 commit 92c06b4

8 files changed

Lines changed: 41 additions & 8 deletions

File tree

cmd/deployments/helpers.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ func pickDeployment(client *api.APIClient, projectID string) (string, error) {
5555

5656
options := make([]string, len(deployments))
5757
for i, d := range deployments {
58-
label := fmt.Sprintf("%s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, d.ID[:8])
58+
id := d.ID
59+
if len(id) > 8 {
60+
id = id[:8]
61+
}
62+
label := fmt.Sprintf("%s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, id)
5963
if d.Source != nil && d.Source.Commit != "" {
6064
commit := d.Source.Commit
6165
if len(commit) > 7 {
@@ -65,7 +69,7 @@ func pickDeployment(client *api.APIClient, projectID string) (string, error) {
6569
if len(msg) > 50 {
6670
msg = msg[:50] + "…"
6771
}
68-
label = fmt.Sprintf("%s %s %s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, d.ID[:8], commit, msg)
72+
label = fmt.Sprintf("%s %s %s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, id, commit, msg)
6973
}
7074
options[i] = label
7175
}

cmd/domains/domains_list.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ func newDomainsListCommand() *cli.Command {
5858
if name, ok := envName[*d.EnvironmentID]; ok {
5959
env = name
6060
} else {
61-
env = (*d.EnvironmentID)[:8]
61+
env = *d.EnvironmentID
62+
if len(env) > 8 {
63+
env = env[:8]
64+
}
6265
}
6366
}
6467
tableData = append(tableData, []string{d.ID, d.Name, env, icon + " " + d.Status, msg})

cmd/env/pull.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package env
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"sort"
78
"strings"
89

@@ -38,6 +39,10 @@ func newEnvPullCommand() *cli.Command {
3839
filePath = ".env." + env.UniqueName
3940
}
4041

42+
if filepath.IsAbs(filePath) || strings.Contains(filePath, "..") {
43+
return fmt.Errorf("--file must be a relative path without '..' (got %q)", filePath)
44+
}
45+
4146
// Check if file exists
4247
if !c.Bool("force") {
4348
if _, err := os.Stat(filePath); err == nil {

cmd/env/push.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package env
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"strings"
78

89
"github.com/pterm/pterm"
@@ -37,6 +38,10 @@ func newEnvPushCommand() *cli.Command {
3738
filePath = ".env." + env.UniqueName
3839
}
3940

41+
if filepath.IsAbs(filePath) || strings.Contains(filePath, "..") {
42+
return fmt.Errorf("--file must be a relative path without '..' (got %q)", filePath)
43+
}
44+
4045
data, err := os.ReadFile(filePath) //nolint:gosec
4146
if err != nil {
4247
return fmt.Errorf("could not read %s: %w", filePath, err)

cmd/open/open.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ func NewOpenCommand() *cli.Command {
9191
} else {
9292
options := make([]string, len(deploymentsWithURL))
9393
for i, d := range deploymentsWithURL {
94-
options[i] = fmt.Sprintf("%s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, d.ID[:8])
94+
id := d.ID
95+
if len(id) > 8 {
96+
id = id[:8]
97+
}
98+
options[i] = fmt.Sprintf("%s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, id)
9599
}
96100
selected, err := pterm.DefaultInteractiveSelect.
97101
WithOptions(options).

cmd/root/root.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package root
33

44
import (
55
"fmt"
6+
"net/url"
67
"time"
78

89
"github.com/urfave/cli/v2"
@@ -65,6 +66,14 @@ func NewApp() *cli.App {
6566
// Store the output format in metadata
6667
c.App.Metadata[output.FormatKey] = output.DetectFormat(c)
6768

69+
apiURL := c.String("api-url")
70+
if apiURL != "" && apiURL != api.DefaultBaseURL {
71+
parsed, err := url.Parse(apiURL)
72+
if err != nil || (parsed.Scheme != "https" && parsed.Hostname() != "localhost" && parsed.Hostname() != "127.0.0.1") {
73+
return fmt.Errorf("--api-url must use HTTPS (got %q)\n\n Exception: localhost and 127.0.0.1 are allowed for development", apiURL)
74+
}
75+
}
76+
6877
cmd := c.Args().First()
6978
if cmd == "" || cmd == "login" || cmd == "logout" || cmd == "version" || cmd == "completion" || cmd == "ask" {
7079
return nil

cmd/templates/use.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ func newTemplatesUseCommand() *cli.Command {
9393

9494
dir := c.String("dir")
9595
if dir == "" {
96-
dir = tmpl.Name
96+
dir = filepath.Base(tmpl.Name)
97+
if dir == "." || dir == ".." {
98+
return fmt.Errorf("template name %q is not safe as a directory name — use --dir to specify output directory", tmpl.Name)
99+
}
97100
}
98101

99102
absDir, err := filepath.Abs(dir)

internal/api/methods.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ func (c *APIClient) GetSkillDownloadURL(purchasedID string) (string, error) {
145145
// PaginatedResponse wraps a paginated list API response envelope.
146146
type PaginatedResponse[T any] struct {
147147
Data struct {
148-
Items []T `json:"data"`
148+
Items []T `json:"data"`
149+
Pagination Pagination `json:"pagination"`
149150
} `json:"data"`
150-
Pagination Pagination `json:"pagination"`
151151
}
152152

153153
// Pagination holds metadata about a paginated response.
@@ -178,7 +178,7 @@ func (c *APIClient) ListAvailableSkillsForPurchase(searchText string, offset int
178178
if resp.IsError() {
179179
return nil, Pagination{}, ParseAPIError(resp.StatusCode(), resp.Body())
180180
}
181-
return result.Data.Items, result.Pagination, nil
181+
return result.Data.Items, result.Data.Pagination, nil
182182
}
183183

184184
// DeploymentExtra holds extra deployment metadata.

0 commit comments

Comments
 (0)