diff --git a/.changes/next-release/enhancement-core-zf6kl82i.json b/.changes/next-release/enhancement-core-zf6kl82i.json new file mode 100644 index 0000000..4fa1a93 --- /dev/null +++ b/.changes/next-release/enhancement-core-zf6kl82i.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "core", + "description": "Warn when --endpoint-url points outside the trusted vngcloud.vn domain, since grn sends a replayable IAM bearer token with every request (SEC-08). The warning does not block the request" +} diff --git a/go/internal/cli/client.go b/go/internal/cli/client.go index b7be48d..27f8138 100644 --- a/go/internal/cli/client.go +++ b/go/internal/cli/client.go @@ -40,6 +40,7 @@ func NewClient(cmd *cobra.Command, serviceName string) (*client.GreenodeClient, var baseURL string if endpointURL != "" { + WarnIfUntrustedEndpoint(endpointURL) baseURL = endpointURL } else { baseURL, err = cfg.GetEndpoint(serviceName) diff --git a/go/internal/cli/endpoint.go b/go/internal/cli/endpoint.go new file mode 100644 index 0000000..648d3d4 --- /dev/null +++ b/go/internal/cli/endpoint.go @@ -0,0 +1,46 @@ +package cli + +import ( + "fmt" + "net/url" + "os" + "strings" +) + +// trustedEndpointSuffix is the domain grn's own services live under. Requests to +// hosts outside it are flagged because grn sends a reusable IAM bearer token +// with every request (see WarnIfUntrustedEndpoint). +const trustedEndpointSuffix = ".vngcloud.vn" + +// IsTrustedEndpoint reports whether endpointURL targets a host within the +// trusted vngcloud.vn domain. An empty value means no --endpoint-url override +// was given (the built-in region endpoint is used), which is trusted. +func IsTrustedEndpoint(endpointURL string) bool { + if endpointURL == "" { + return true + } + u, err := url.Parse(endpointURL) + if err != nil { + return false + } + host := u.Hostname() + if host == "" { + return false + } + return host == "vngcloud.vn" || strings.HasSuffix(host, trustedEndpointSuffix) +} + +// WarnIfUntrustedEndpoint prints a security warning to stderr when endpointURL +// points outside the trusted vngcloud.vn domain. grn authenticates against the +// real IAM and sends the resulting reusable bearer token to whatever host +// --endpoint-url names, so a malicious or mistyped host can capture and replay +// that token. This warns; it does not block. +func WarnIfUntrustedEndpoint(endpointURL string) { + if IsTrustedEndpoint(endpointURL) { + return + } + u, _ := url.Parse(endpointURL) + fmt.Fprintf(os.Stderr, + "Warning: --endpoint-url %q is outside the trusted %s domain. grn will send your IAM bearer token to this host, and a bearer token can be replayed. Only use endpoints you trust.\n", + u.Hostname(), strings.TrimPrefix(trustedEndpointSuffix, ".")) +} diff --git a/go/internal/cli/endpoint_test.go b/go/internal/cli/endpoint_test.go new file mode 100644 index 0000000..63e188b --- /dev/null +++ b/go/internal/cli/endpoint_test.go @@ -0,0 +1,21 @@ +package cli + +import "testing" + +func TestIsTrustedEndpoint(t *testing.T) { + cases := map[string]bool{ + "": true, // no override -> built-in endpoint + "https://vks.api.vngcloud.vn": true, + "https://hcm-3.api.vngcloud.vn/x": true, + "https://vngcloud.vn": true, + "http://attacker.com": false, + "https://evil.vngcloud.vn.attacker.com": false, // suffix must be a real domain boundary + "http://localhost:8080": false, + "not-a-url ::::": false, + } + for in, want := range cases { + if got := IsTrustedEndpoint(in); got != want { + t.Errorf("IsTrustedEndpoint(%q) = %v, want %v", in, got, want) + } + } +} diff --git a/go/internal/vserverclient/client.go b/go/internal/vserverclient/client.go index f93f000..575709d 100644 --- a/go/internal/vserverclient/client.go +++ b/go/internal/vserverclient/client.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/vngcloud/greennode-cli/internal/auth" + "github.com/vngcloud/greennode-cli/internal/cli" "github.com/vngcloud/greennode-cli/internal/client" "github.com/vngcloud/greennode-cli/internal/config" "github.com/vngcloud/greennode-cli/internal/formatter" @@ -37,6 +38,7 @@ func BuildClient(cmd *cobra.Command) (*client.GreenodeClient, *config.Config, er var baseURL string if endpointURL != "" { + cli.WarnIfUntrustedEndpoint(endpointURL) baseURL = endpointURL } else { baseURL, err = cfg.GetEndpoint("vserver")