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{}