Root cause
Injection & unsafe execution
feat(management): add HTTP REST API for external management tools
How AI contributed
New attack surfaceA vulnerability was identified in chenhg5 cc-connect up to 1.4.1. This affects the function shellExecCommand of the file core/engine.go of the component Management API. Such manipulation of the argument exec leads to os command injection. It is possible to launch the attack remotely. The exploit is publicly available and might be used. The reported GitHub issue was closed automatically due to inactivity.
When the Management API is enabled with its token omitted or empty, its middleware treats every request as authenticated and its HTTP server listens on all interfaces. A remote caller can POST an arbitrary exec string to /api/v1/cron; that string is persisted as CronJob.Exec and executed by the cron scheduler through shellExecCommand with the shell command flag (-c on Unix). Scheduled execution is sufficient for RCE, and later commit c580b68716a7cdc6a0e6702e5782c17d074a7978 also added POST /api/v1/cron/{id}/exec for immediate execution. No shell metacharacter neutralization or command allowlist intervenes.
Root cause
feat(management): add HTTP REST API for external management tools
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
@@ -0,0 +1,459 @@+package core++import (+ "bytes"+ "encoding/json"+ "net/http"+ "net/http/httptest"+ "strings"+ "testing"+)++// testManagementServer creates a ManagementServer with a test engine and returns an httptest.Server.+func testManagementServer(t *testing.T, token string) (*ManagementServer, *httptest.Server, *Engine) {+ t.Helper()++ agent := &stubAgent{}+ sm := NewSessionManager("")+ e := NewEngine("test-project", agent, nil, "", LangEnglish)+ e.sessions = sm++ mgmt := NewManagementServer(0, token, nil)+ mgmt.RegisterEngine("test-project", e)++ mux := http.NewServeMux()+ prefix := "/api/v1"+ mux.HandleFunc(prefix+"/status", mgmt.wrap(mgmt.handleStatus))+ mux.HandleFunc(prefix+"/restart", mgmt.wrap(mgmt.handleRestart))+ mux.HandleFunc(prefix+"/reload", mgmt.wrap(mgmt.handleReload))+ mux.HandleFunc(prefix+"/config", mgmt.wrap(mgmt.handleConfig))+ mux.HandleFunc(prefix+"/projects", mgmt.wrap(mgmt.handleProjects))+ mux.HandleFunc(prefix+"/projects/", mgmt.wrap(mgmt.handleProjectRoutes))+ mux.HandleFunc(prefix+"/cron", mgmt.wrap(mgmt.handleCron))+ mux.HandleFunc(prefix+"/cron/", mgmt.wrap(mgmt.handleCronByID))+ mux.HandleFunc(prefix+"/bridge/adapters", mgmt.wrap(mgmt.handleBridgeAdapters))++ ts := httptest.NewServer(mux)+ t.Cleanup(ts.Close)++ return mgmt, ts, e+}++type mgmtResponse struct {+ OK bool `json:"ok"`+ Data json.RawMessage `json:"data,omitempty"`+ Error string `json:"error,omitempty"`+}++func mgmtGet(t *testing.T, url, token string) mgmtResponse {+ t.Helper()+ req, _ := http.NewRequest("GET", url, nil)+ if token != "" {+ req.Header.Set("Authorization", "Bearer "+token)+ }+ resp, err := http.DefaultClient.Do(req)+ if err != nil {+ t.Fatalf("GET %s: %v", url, err)+ }+ defer resp.Body.Close()+ var r mgmtResponse+ json.NewDecoder(resp.Body).Decode(&r)+ return r+}++func mgmtPost(t *testing.T, url, token string, body any) mgmtResponse {+ t.Helper()+ var buf bytes.Buffer+ if body != nil {+ json.NewEncoder(&buf).Encode(body)+ }+ req, _ := http.NewRequest("POST", url, &buf)+ req.Header.Set("Content-Type", "application/json")+ if token != "" {+ req.Header.Set("Authorization", "Bearer "+token)+ }+ resp, err := http.DefaultClient.Do(req)+ if err != nil {+ t.Fatalf("POST %s: %v", url, err)+ }+ defer resp.Body.Close()+ var r mgmtResponse+ json.NewDecoder(resp.Body).Decode(&r)+ return r+}++func mgmtPatch(t *testing.T, url, token string, body any) mgmtResponse {+ t.Helper()+ var buf bytes.Buffer+ if body != nil {+ json.NewEncoder(&buf).Encode(body)@@ -0,0 +1,1067 @@+package core++import (+ "crypto/subtle"+ "encoding/json"+ "fmt"+ "log/slog"+ "net/http"+ "strconv"+ "strings"+ "sync"+ "time"+)++// ManagementServer provides an HTTP REST API for external management tools+// (web dashboards, TUI clients, GUI desktop apps, Mac tray apps, etc.).+type ManagementServer struct {+ port int+ token string+ corsOrigins []string+ server *http.Server+ startedAt time.Time++ mu sync.RWMutex+ engines map[string]*Engine // project name → engine++ cronScheduler *CronScheduler+ heartbeatScheduler *HeartbeatScheduler+ bridgeServer *BridgeServer+ logBuffer *logRingBuffer+}++// NewManagementServer creates a new management API server.+func NewManagementServer(port int, token string, corsOrigins []string) *ManagementServer {+ return &ManagementServer{+ port: port,+ token: token,+ corsOrigins: corsOrigins,+ engines: make(map[string]*Engine),+ startedAt: time.Now(),+ logBuffer: newLogRingBuffer(500),+ }+}++func (m *ManagementServer) RegisterEngine(name string, e *Engine) {+ m.mu.Lock()+ defer m.mu.Unlock()+ m.engines[name] = e+}++func (m *ManagementServer) SetCronScheduler(cs *CronScheduler) { m.cronScheduler = cs }+func (m *ManagementServer) SetHeartbeatScheduler(hs *HeartbeatScheduler) { m.heartbeatScheduler = hs }+func (m *ManagementServer) SetBridgeServer(bs *BridgeServer) { m.bridgeServer = bs }++func (m *ManagementServer) Start() {+ mux := http.NewServeMux()+ prefix := "/api/v1"++ // System+ mux.HandleFunc(prefix+"/status", m.wrap(m.handleStatus))+ mux.HandleFunc(prefix+"/restart", m.wrap(m.handleRestart))+ mux.HandleFunc(prefix+"/reload", m.wrap(m.handleReload))+ mux.HandleFunc(prefix+"/config", m.wrap(m.handleConfig))++ // Projects+ mux.HandleFunc(prefix+"/projects", m.wrap(m.handleProjects))+ mux.HandleFunc(prefix+"/projects/", m.wrap(m.handleProjectRoutes))++ // Cron (global)+ mux.HandleFunc(prefix+"/cron", m.wrap(m.handleCron))+ mux.HandleFunc(prefix+"/cron/", m.wrap(m.handleCronByID))++ // Bridge+ mux.HandleFunc(prefix+"/bridge/adapters", m.wrap(m.handleBridgeAdapters))++ m.server = &http.Server{+ Addr: fmt.Sprintf(":%d", m.port),+ Handler: mux,+ }+ go func() {+ if err := m.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {+ slog.Error("management api server error", "error", err)+ }+ }()+ slog.Info("management api started", "port", m.port)+}++func (m *ManagementServer) Stop() {+ if m.server != nil {@@ -32,6 +32,7 @@ type Config struct { Cron CronConfig `toml:"cron"` Webhook WebhookConfig `toml:"webhook"` Bridge BridgeConfig `toml:"bridge"`+ Management ManagementConfig `toml:"management"` IdleTimeoutMins *int `toml:"idle_timeout_mins,omitempty"` // max minutes between agent events; 0 = no timeout; default 120 } @@ -56,6 +57,14 @@ type BridgeConfig struct { Path string `toml:"path,omitempty"` // URL path; default "/bridge/ws" } +// ManagementConfig controls the HTTP Management API for external tools.+type ManagementConfig struct {+ Enabled *bool `toml:"enabled"` // default false+ Port int `toml:"port,omitempty"` // listen port; default 9820+ Token string `toml:"token,omitempty"` // shared secret for authentication; required+ CORSOrigins []string `toml:"cors_origins,omitempty"` // allowed CORS origins; empty = no CORS+}+ // DisplayConfig controls how intermediate messages (thinking, tool output) are shown. type DisplayConfig struct { ThinkingMaxLen *int `toml:"thinking_max_len"` // max chars for thinking messages; 0 = no truncation; default 300Advisory references