Files
cc-connect/core/privileged_test.go
T
d484ae64fc feat(core): gate /commands addexec and /cron addexec behind admin_from (#1036)
Both /commands and /cron currently have a single coarse-grained
privilege check: the cmdID is or is not in privilegedCommands. Neither
is listed there, so all subcommands run for any user — including
`addexec`, which registers a custom command that shells out, or
schedules one that shells out on a cron. A non-admin user who can send
slash commands can effectively install arbitrary shell payloads for
anyone else to trigger.

(executeCustomCommand already gates Exec-bearing custom commands on
admin_from after they have been registered, so executing such a payload
also requires admin — but the registration itself shouldn't be open to
non-admins either. This PR closes the registration side.)

Introduce isPrivilegedCommandInvocation(cmdID, args) which:

  - falls through to the existing privilegedCommands map for the
    static cases (/shell, /show, /dir, /restart, /upgrade, /web, /diff)
  - additionally returns true for `/commands addexec ...` and
    `/cron addexec ...` (subcommand names matched via matchSubCommand
    so prefix forms like `/cron addex` and case variants are covered)
  - returns false for every other subcommand of /commands and /cron
    (list, add, del, etc. stay non-privileged)

handleCommand swaps the privilegedCommands[cmdID] check for the new
helper. No other privilege paths change.

Co-authored-by: Shuchao Shao <shaoshch@yonyou.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-14 08:35:35 +08:00

107 lines
3.5 KiB
Go

package core
import (
"strings"
"testing"
)
func TestIsPrivilegedCommandInvocation_StaticListUnchanged(t *testing.T) {
for cmd := range privilegedCommands {
if !isPrivilegedCommandInvocation(cmd, nil) {
t.Errorf("%q should still be privileged even without args", cmd)
}
if !isPrivilegedCommandInvocation(cmd, []string{"whatever"}) {
t.Errorf("%q should still be privileged with arbitrary args", cmd)
}
}
}
func TestIsPrivilegedCommandInvocation_CommandsSubcommandGate(t *testing.T) {
if isPrivilegedCommandInvocation("commands", nil) {
t.Fatal("/commands without subcommand must not be privileged")
}
for _, sub := range []string{"list", "add", "del", "delete", "rm", "remove"} {
if isPrivilegedCommandInvocation("commands", []string{sub}) {
t.Errorf("/commands %s must NOT require admin", sub)
}
}
for _, sub := range []string{"addexec", "ADDEXEC", "addEx"} {
if !isPrivilegedCommandInvocation("commands", []string{sub}) {
t.Errorf("/commands %s must require admin", sub)
}
}
}
func TestIsPrivilegedCommandInvocation_CronSubcommandGate(t *testing.T) {
if isPrivilegedCommandInvocation("cron", nil) {
t.Fatal("/cron without subcommand must not be privileged")
}
for _, sub := range []string{"add", "list", "del", "enable", "disable", "mute", "unmute", "setup"} {
if isPrivilegedCommandInvocation("cron", []string{sub}) {
t.Errorf("/cron %s must NOT require admin", sub)
}
}
for _, sub := range []string{"addexec", "ADDEXEC"} {
if !isPrivilegedCommandInvocation("cron", []string{sub}) {
t.Errorf("/cron %s must require admin", sub)
}
}
}
func TestIsPrivilegedCommandInvocation_UnknownCmdNotPrivileged(t *testing.T) {
if isPrivilegedCommandInvocation("help", []string{"addexec"}) {
t.Error("/help addexec is not a real privileged path; only commands/cron addexec are")
}
}
func TestHandleCommand_CommandsAddexecBlocksNonAdmin(t *testing.T) {
p := &stubPlatformEngine{n: "test"}
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
e.SetAdminFrom("") // no admins → addexec must be denied for everyone
msg := &Message{UserID: "u", Platform: "test", ReplyCtx: "rctx"}
handled := e.handleCommand(p, msg, "/commands addexec foo bar")
if !handled {
t.Fatal("addexec invocation must be intercepted, not forwarded to agent")
}
sent := p.getSent()
if len(sent) == 0 || !strings.Contains(strings.ToLower(sent[0]), "admin") {
t.Fatalf("expected admin-required reply; got %#v", sent)
}
}
func TestHandleCommand_CommandsListNoAdmin(t *testing.T) {
p := &stubPlatformEngine{n: "test"}
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
e.SetAdminFrom("")
msg := &Message{UserID: "u", Platform: "test", ReplyCtx: "rctx"}
handled := e.handleCommand(p, msg, "/commands list")
if !handled {
t.Fatal("/commands list must be handled by cc-connect")
}
for _, s := range p.getSent() {
if strings.Contains(strings.ToLower(s), "admin") &&
strings.Contains(strings.ToLower(s), "required") {
t.Errorf("/commands list must NOT be admin-gated; got %q", s)
}
}
}
func TestHandleCommand_CronAddexecBlocksNonAdmin(t *testing.T) {
p := &stubPlatformEngine{n: "test"}
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
e.SetAdminFrom("")
msg := &Message{UserID: "u", Platform: "test", ReplyCtx: "rctx"}
handled := e.handleCommand(p, msg, "/cron addexec daily echo hi")
if !handled {
t.Fatal("cron addexec must be intercepted")
}
sent := p.getSent()
if len(sent) == 0 || !strings.Contains(strings.ToLower(sent[0]), "admin") {
t.Fatalf("expected admin-required reply; got %#v", sent)
}
}