#1852·cc-connect

Feishu card-action handler bypasses the allow_from user allowlist

Author: carfeiiCreated Sep 15, 2026Updated Sep 15, 2026

Summary

cc-connect bridges local AI coding agents to messaging platforms and gates who may command the agent with a per-platform allow_from setting. Every platform's plain-text message path enforces it. The Feishu platform additionally supports interactive message "cards" (buttons, forms, permission-approval prompts), handled by onCardAction(). That function checks only the chat-level allow_chat filter and never checks allow_from, so any user who can see and click a card the bot has posted, in any chat the operator's allow_chat config admits (which may include a shared group), can trigger a full agent command dispatch or approve the agent's own pending dangerous-action confirmation, entirely bypassing the per-user allowlist that protects the equivalent text-message path.

Affected versions: confirmed on commit 757b4df (current main).

Details

platform/feishu/feishu.go, the plain-text message handler correctly enforces both filters, and documents the intended model:

go
// With history sharing enabled, observe ordinary text/post messages only
// after the chat-level allow list has admitted the chat. Do this before the
// existing mention filter and before allow_from: the chat controls whether
// cc-connect may observe the conversation, while allow_from controls who may
// trigger an agent turn.
...
if !core.AllowList(p.allowFrom, userID) {
	slog.Debug(p.tag()+": message from unauthorized user", "user", userID)
	p.replyUnauthorizedAccess(ctx, replyContext{...})
	return nil
}

if chatType == "group" && !core.AllowList(p.allowChat, chatID) {
	slog.Debug(p.tag()+": message from unauthorized chat", "chat_id", chatID)
	return nil
}

onCardAction(), as shipped, checks only allow_chat:

go
func (p *Platform) onCardAction(event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
	if event.Event == nil || event.Event.Action == nil {
		return nil, nil
	}

	// Check allow_chat filter: skip card actions from chats this platform doesn't own.
	if event.Event.Context != nil && event.Event.Context.OpenChatID != "" {
		if !core.AllowList(p.allowChat, event.Event.Context.OpenChatID) {
			return nil, nil
		}
	}

	actionVal, _ := event.Event.Action.Value["action"].(string)
	...

No allow_from check follows, for any of the five action prefixes the function handles:

  • cmd: dispatches the button's payload as an arbitrary agent command, identical in effect to typing that text:
    go
    if strings.HasPrefix(actionVal, "cmd:") {
        cmdText := strings.TrimPrefix(actionVal, "cmd:")
        ...
        go p.dispatchCoreMessage(&core.Message{..., Content: cmdText, ...})
    }
  • perm:allow / perm:deny / perm:allow_all approve or deny the agent's own pending permission/confirmation prompt:
    go
    if strings.HasPrefix(actionVal, "perm:") {
        ...
        go p.dispatchCoreMessage(&core.Message{..., Content: responseText, IsPermissionResponse: true, ...})
    }
  • nav:, act:, and askq: similarly dispatch to the agent or update agent-facing state without any allow_from check.

dispatchCoreMessage() hands the message straight to the registered handler with no further authorization check downstream, so nothing else in the call chain re-validates the sender.

POC

(available upon request)

Impact

Any user who can see a card the bot has posted, in a chat admitted by allow_chat (which is frequently a shared group, not a 1:1 DM, and defaults to allow-all when unset), can:

  • Click a cmd: button to dispatch an arbitrary command to the agent as if they were the allow_from-authorized operator.
  • Click perm:allow or perm:allow_all to approve the agent's own pending confirmation for a dangerous action (the human-in-the-loop safety check the permission-prompt UI exists to enforce), on behalf of the operator, without being that operator.
  • Click nav:/act:/askq: buttons to navigate cards, run in-place actions, or answer the agent's own questions, steering its behavior.

This defeats the allow_from allowlist that is the project's own documented mechanism for restricting who may command the agent, whenever the agent runs on Feishu and posts any interactive card (which is the default behavior; interactive cards are enabled unless explicitly disabled via enable_feishu_card: false).