This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new bd1dafabf fix(rmqctl): reject embedded credentials in context server 
URLs (#6689)
bd1dafabf is described below

commit bd1dafabfcb38b2ee4d84db3d3dd4d2e8eb46bdb
Author: coder999o <[email protected]>
AuthorDate: Mon Oct 19 20:08:42 2026 +0800

    fix(rmqctl): reject embedded credentials in context server URLs (#6689)
    
    Verified against `rocketmq-studio` at ecd7d62ad before merging: the defect 
is still present on the
    current trunk and the change merges cleanly.
    
    Maintainer follow-ups on top of the contributor commit:
    - fix(rmqctl): reject a query or fragment in --server before it is written
    
    Each follow-up commit message records the review finding it addresses.
    
    Locally verified: targeted suites green on the integration branch; a full 
backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 rmqctl/cmd/config.go      | 19 ++++++++++
 rmqctl/cmd/config_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 111 insertions(+)

diff --git a/rmqctl/cmd/config.go b/rmqctl/cmd/config.go
index 0f264fec5..f2ad45092 100644
--- a/rmqctl/cmd/config.go
+++ b/rmqctl/cmd/config.go
@@ -8,6 +8,7 @@ package cmd
 
 import (
        "fmt"
+       "net/url"
        "strings"
 
        "github.com/apache/rocketmq-dashboard/rmqctl/internal/config"
@@ -37,6 +38,24 @@ func newConfigSetContextCommand(runtime commandRuntime) 
*cobra.Command {
                Short: "Create or update a complete Studio access context",
                Args:  cobra.ExactArgs(1),
                RunE: func(cmd *cobra.Command, args []string) error {
+                       if cmd.Flags().Changed("server") {
+                               parsed, err := url.Parse(server)
+                               if err != nil {
+                                       return invalidArgument("--server must 
be a valid Studio Server URL")
+                               }
+                               if parsed.User != nil {
+                                       return invalidArgument("--server must 
not contain user info; use environment credential references")
+                               }
+                               // studio.Target.validate() rejects a query and 
a fragment at request time. Without the
+                               // same guard here "--server 
https://host/?accessKey=..."; is written to the config file
+                               // and echoed back by `config get-contexts` in 
plain text.
+                               if parsed.RawQuery != "" {
+                                       return invalidArgument("--server must 
not contain a query; use environment credential references")
+                               }
+                               if parsed.Fragment != "" {
+                                       return invalidArgument("--server must 
not contain a fragment; use environment credential references")
+                               }
+                       }
                        path, cfg, err := runtime.configForWrite()
                        if err != nil {
                                return err
diff --git a/rmqctl/cmd/config_test.go b/rmqctl/cmd/config_test.go
index 155d1f43a..47c1b5092 100644
--- a/rmqctl/cmd/config_test.go
+++ b/rmqctl/cmd/config_test.go
@@ -18,6 +18,7 @@ package cmd
 
 import (
        "bytes"
+       "fmt"
        "os"
        "strings"
        "testing"
@@ -87,6 +88,97 @@ func TestConfigSetContextRejectsRemovedClusterFlag(t 
*testing.T) {
        }
 }
 
+func TestConfigSetContextRejectsServerCredentialsBeforeSaving(t *testing.T) {
+       for _, server := range []string{
+               "https://example-user:[email protected]";,
+               "https://[email protected]";,
+               "https://:[email protected]";,
+               "https://@studio.example.com";,
+               "https://example-user:[email protected]";,
+               "https://studio.example.com/?accessKey=example-password";,
+               "https://studio.example.com/#secretKey=example-password";,
+       } {
+               for _, existing := range []bool{false, true} {
+                       t.Run(fmt.Sprintf("%s/existing=%t", server, existing), 
func(t *testing.T) {
+                               stdout, stderr := &bytes.Buffer{}, 
&bytes.Buffer{}
+                               app := NewApp(stdout, stderr)
+                               app.Store.Getenv = emptyEnv
+                               configPath := newTestConfigPath(t)
+                               var before []byte
+                               if existing {
+                                       cfg := config.Config{CurrentContext: 
"prod", Contexts: map[string]config.Context{
+                                               "prod": {Server: 
"https://studio.example.com";, Credential: config.CredentialRef{
+                                                       AccessKeyRef: 
"env:RMQ_AK", SecretKeyRef: "env:RMQ_SK",
+                                               }},
+                                       }}
+                                       if err := app.Store.Save(configPath, 
cfg); err != nil {
+                                               t.Fatal(err)
+                                       }
+                                       var err error
+                                       before, err = os.ReadFile(configPath)
+                                       if err != nil {
+                                               t.Fatal(err)
+                                       }
+                               }
+                               args := []string{"--config", configPath, 
"config", "set-context", "prod", "--server", server}
+                               if !existing {
+                                       args = append(args, "--access-key-env", 
"RMQ_AK", "--secret-key-env", "RMQ_SK")
+                               }
+                               if exitCode := app.Execute(args); exitCode == 0 
{
+                                       t.Fatal("set-context accepted server 
URL credentials")
+                               }
+                               if stdout.Len() != 0 || 
!strings.Contains(stderr.String(), "INVALID_ARGUMENT") || 
!strings.Contains(stderr.String(), "--server") {
+                                       t.Fatalf("stdout=%q stderr=%q, want an 
argument error without success output", stdout, stderr)
+                               }
+                               if strings.Contains(stderr.String(), 
"example-password") || strings.Contains(stderr.String(), "example-user") {
+                                       t.Fatal("error output disclosed URL 
credentials")
+                               }
+                               after, err := os.ReadFile(configPath)
+                               if existing {
+                                       if err != nil || !bytes.Equal(before, 
after) {
+                                               t.Fatalf("rejected update 
changed the existing config: %v", err)
+                                       }
+                               } else if !os.IsNotExist(err) {
+                                       t.Fatalf("rejected creation wrote 
config: %v", err)
+                               }
+                       })
+               }
+       }
+}
+
+func TestConfigSetContextCanRemoveStoredServerCredentials(t *testing.T) {
+       stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
+       app := NewApp(stdout, stderr)
+       app.Store.Getenv = emptyEnv
+       configPath := newTestConfigPath(t)
+       if err := app.Store.Save(configPath, config.Config{CurrentContext: 
"prod", Contexts: map[string]config.Context{
+               "prod": {Server: 
"https://example-user:[email protected]";, Credential: 
config.CredentialRef{
+                       AccessKeyRef: "env:RMQ_AK", SecretKeyRef: "env:RMQ_SK",
+               }},
+               "dev": {Server: "http://localhost:8888";, Credential: 
config.CredentialRef{
+                       AccessKeyRef: "env:DEV_AK", SecretKeyRef: "env:DEV_SK",
+               }},
+       }}); err != nil {
+               t.Fatal(err)
+       }
+       if exitCode := app.Execute([]string{"--config", configPath, "config", 
"set-context", "prod",
+               "--server", "https://studio.example.com/studio"}); exitCode != 
0 {
+               t.Fatalf("repair failed: %s", stderr)
+       }
+       cfg, err := app.Store.Load(configPath)
+       if err != nil {
+               t.Fatal(err)
+       }
+       contextValue := cfg.Contexts["prod"]
+       if contextValue.Server != "https://studio.example.com/studio"; ||
+               contextValue.Credential.AccessKeyRef != "env:RMQ_AK" || 
contextValue.Credential.SecretKeyRef != "env:RMQ_SK" {
+               t.Fatalf("repair did not preserve context fields: %#v", 
contextValue)
+       }
+       if cfg.CurrentContext != "prod" || len(cfg.Contexts) != 2 || 
cfg.Contexts["dev"].Server != "http://localhost:8888"; {
+               t.Fatalf("repair changed unrelated configuration: %#v", cfg)
+       }
+}
+
 func TestConfigDeleteContextRemovesContextAndClearsCurrent(t *testing.T) {
        stdout := &bytes.Buffer{}
        stderr := &bytes.Buffer{}

Reply via email to