Skip to content

fix(feishu): carry forward DM fallback and topic labels#73399

Open
vincentkoc wants to merge 2 commits intomainfrom
clownfish/ghcrawl-156980-autonomous-smoke
Open

fix(feishu): carry forward DM fallback and topic labels#73399
vincentkoc wants to merge 2 commits intomainfrom
clownfish/ghcrawl-156980-autonomous-smoke

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

Carry forward the remaining Feishu display-name behavior from #38958 after #51032 already landed the group-name session label fix.

This replacement should stay narrow:

  • keep feat(feishu): display group name instead of chat_id in session labels #51032's group-name implementation as the baseline
  • add/repair DM display-name fallback through chatMembers with contact fallback
  • preserve resolveSenderNames opt-out semantics
  • scope sender/direct-member caches by account and relevant chat/sender identifiers
  • surface topic/root labels only for topic-scoped Feishu sessions
  • propagate permission guidance when DM fallback lookup hits a Feishu permission error

Credit: this work is based on the implementation direction and review iterations from @futuremind2026 in #38958. The already-merged group-name baseline came from @jnuyao in #51032.

Validation

  • pnpm exec vitest run --config vitest.extensions.config.ts extensions/feishu/src/bot.test.ts extensions/feishu/src/bot-sender-name.test.ts
  • pnpm exec vitest run src/config/sessions.test.ts
  • pnpm check:changed

ProjectClownfish replacement details:

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 28, 2026

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Potential sender name misattribution in Feishu DM member lookup fallback
2 🟡 Medium Unbounded in-memory caches for Feishu sender/direct-member names can cause memory growth (DoS)
3 🟡 Medium Untrusted Feishu error URL injected into agent System prompt (phishing/prompt injection)
1. 🟡 Potential sender name misattribution in Feishu DM member lookup fallback
Property Value
Severity Medium
CWE CWE-345
Location extensions/feishu/src/bot-sender-name.ts:140-144

Description

resolveFeishuDirectMemberName can return a display name for a sender even when the sender’s member_id is not present in the returned member list.

  • Input: senderId comes from event payload and is used to identify the sender.
  • Lookup: code calls im.chatMembers.get with page_size: 10.
  • Risky fallback: if no member_id matches senderId and the API returns exactly one member, the code uses that member’s name anyway.
  • Impact: in p2p/private chats, if the API returns an incomplete/partial member list (paging behavior changes, transient errors, permission-scoped responses, or other anomalies), the bot may attribute the sender’s messages to the wrong human name in transcripts/logs. This is an integrity issue that could enable sender-name spoofing/misattribution.

Vulnerable code:

const matchedMember =
  members.find((member) => member.member_id?.trim() === senderId) ??
  (members.length === 1 ? members[0] : undefined);

Recommendation

Remove the non-matching single-item fallback, or make it safe by ensuring the selected member is actually the sender.

Safer approach (only accept exact match; otherwise fall back to contact lookup):

const matched = members.find(m => m.member_id?.trim() === senderId);
const name = normalizeName(matched?.name);
if (!name) return {};
return { name };

If you need to handle pagination/incomplete results, explicitly page through results until either:

  • the sender is found, or
  • no more pages are available.

Additionally consider logging when the sender cannot be found in the member list for a p2p/private chat to catch API/permission anomalies.

2. 🟡 Unbounded in-memory caches for Feishu sender/direct-member names can cause memory growth (DoS)
Property Value
Severity Medium
CWE CWE-400
Location extensions/feishu/src/bot-sender-name.ts:29-31

Description

bot-sender-name.ts maintains two process-wide Map caches (senderNameCache, directMemberNameCache) keyed by account/chat/sender identifiers.

  • Keys are derived from message context (senderId, chatId, accountId/appId). These values come from Feishu message events and can grow with the number of distinct users/chats observed.
  • Entries are written with an expireAt timestamp, but expired entries are never evicted (no periodic cleanup, no size bound, and no deletion on read when expired).
  • In a long-running process, an attacker (or any user capable of generating many unique chats/users that trigger events) can cause unbounded growth in these Maps, potentially leading to elevated memory usage and eventual process crash/restart (denial of service).

Vulnerable code:

const senderNameCache = new Map<string, { name: string; expireAt: number }>();
const directMemberNameCache = new Map<string, { name: string; expireAt: number }>();
...
directMemberNameCache.set(cacheKey, { name, expireAt: now + SENDER_NAME_TTL_MS });
...
senderNameCache.set(cacheKey, { name, expireAt: now + SENDER_NAME_TTL_MS });

Recommendation

Bound and/or actively prune the caches.

Options:

  1. Evict expired entries on access and periodically prune:
function getWithTtl<T>(m: Map<string, {value: T; expireAt: number}>, k: string): T | undefined {
  const v = m.get(k);
  if (!v) return;
  if (v.expireAt <= Date.now()) {
    m.delete(k);
    return;
  }
  return v.value;
}

function pruneExpired(m: Map<string, {expireAt: number}>, maxToScan = 1000) {
  const now = Date.now();
  let scanned = 0;
  for (const [k, v] of m) {
    if (scanned++ > maxToScan) break;
    if (v.expireAt <= now) m.delete(k);
  }
}
  1. Use an LRU cache with a max size (recommended), e.g. lru-cache:
import LRUCache from "lru-cache";
const senderNameCache = new LRUCache<string, string>({ max: 5000, ttl: SENDER_NAME_TTL_MS });

Also consider limiting caching for DM chat member lookups (cache only by senderId, or cap per-account/per-chat) to reduce key cardinality.

3. 🟡 Untrusted Feishu error URL injected into agent System prompt (phishing/prompt injection)
Property Value
Severity Medium
CWE CWE-451
Location extensions/feishu/src/bot.ts:304-307

Description

extractPermissionError() parses a permission error message coming from a remote Feishu API/HTTP error object and extracts the first matching URL. That URL is not validated to be an official Feishu domain and is later interpolated directly into an LLM-facing [System: ...] string.

Impact:

  • If an attacker can influence the upstream error body (e.g., malicious proxy/MITM, compromised dependency, or any code path that throws an object with code=99991672 and a crafted msg/message), they can cause the bot to present an attacker-controlled https://…/app/… link as an official “permission grant URL”. This is a phishing/social-engineering vector.
  • The URL is also inserted unescaped into a [System: ...] instruction block, enabling prompt-format injection via special characters (e.g., ]) even without whitespace/newlines.

Vulnerable code (extraction + propagation):

  • URL extraction accepts any https://<host>/app/<...> and uses it as grantUrl.
  • buildFeishuAgentBody() embeds grantUrl directly into a [System: ... Permission grant URL: ${grantUrl}] message.

Recommendation

Treat grantUrl as untrusted input.

  1. Validate the domain/host before surfacing it (allowlist open.feishu.cn / open.larksuite.com as appropriate):
function isAllowedFeishuGrantUrl(raw: string): boolean {
  try {
    const u = new URL(raw);
    return (
      u.protocol === "https:" &&
      ["open.feishu.cn", "open.larksuite.com"].includes(u.hostname) &&
      u.pathname.startsWith("/app/")
    );
  } catch {
    return false;
  }
}
  1. Escape/encode before interpolating into LLM/system text, or present it as a structured field instead of free text.

Example usage:

const raw = permissionErrorForAgent.grantUrl;
const safeUrl = raw && isAllowedFeishuGrantUrl(raw) ? raw : undefined;
messageBody += `\n\n[System: ... Permission grant URL: ${safeUrl ?? "(unavailable)"}]`;
  1. Optionally, avoid parsing URLs from msg entirely; prefer a dedicated API field if Feishu provides one.

Analyzed PR: #73399 at commit bdc3b6e

Last updated on: 2026-04-28T07:56:58Z

@openclaw-barnacle openclaw-barnacle Bot added channel: feishu Channel integration: feishu and removed clownfish:human-review labels Apr 28, 2026
@openclaw-barnacle openclaw-barnacle Bot added size: L maintainer Maintainer-authored PR labels Apr 28, 2026
@vincentkoc vincentkoc added the clawsweeper Tracked by ClawSweeper automation label Apr 28, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR carries forward the remaining Feishu DM display-name behavior from #38958 on top of the already-merged group-name baseline from #51032. It adds a resolveFeishuDirectMemberName function that queries the chatMembers API for p2p/private chats before falling back to the contact API, scopes both sender and direct-member caches per account (accountId:appId), propagates permission guidance from the direct-member error path when the contact fallback also misses, and restricts ThreadLabel/MessageThreadId to topic-scoped sessions only. The two P2 notes are about extractPermissionError's broadened error-detection fallback and the absence of negative-hit caching in the direct-member path.

Confidence Score: 4/5

Safe to merge; no runtime-breaking issues found — only minor style/observability improvements suggested.

All findings are P2. The logic for DM fallback, cache scoping, permission-error propagation, and topic-label gating is correct. Tests cover the primary scenarios including per-account cache isolation and stale-scope suppression.

extensions/feishu/src/bot-sender-name.ts — review the extractPermissionError dual-path fallback and the absence of negative-hit caching.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/feishu/src/bot-sender-name.ts
Line: 52

Comment:
**`extractPermissionError` now treats the raw error object as data**

`const data = axiosErr.response?.data ?? err` means any object with `code: 99991672` will now be matched as a Feishu permission error even without an axios `response` wrapper. This is intentional for the non-throw path where `extractPermissionError(res)` is called directly on an API response object (line 132), and it correctly handles the test cases for thrown plain objects. The risk is that a non-Feishu error coincidentally carrying `code: 99991672` would be misidentified — very unlikely given how specific that code is, but worth a brief comment explaining the dual-path design.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/feishu/src/bot-sender-name.ts
Line: 127-130

Comment:
**No negative-hit caching for direct member lookup**

When the API call succeeds but no matching member is found (e.g., `matchedMember` is undefined or the name trims to empty), the function returns `{}` without caching anything. This means every subsequent message in the same DM will trigger another `chatMembers.get` call until a name is resolved. The existing contact (`senderNameCache`) path has the same omission. For DMs this is limited to 2 members per call and the TTL would cap the problem, but consider storing a sentinel or using the same TTL on negative hits to bound API usage when a user genuinely has no display name configured.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(clownfish): address review for ghcra..." | Re-trigger Greptile

}
const axiosErr = err as { response?: { data?: unknown } };
const data = axiosErr.response?.data;
const data = axiosErr.response?.data ?? err;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 extractPermissionError now treats the raw error object as data

const data = axiosErr.response?.data ?? err means any object with code: 99991672 will now be matched as a Feishu permission error even without an axios response wrapper. This is intentional for the non-throw path where extractPermissionError(res) is called directly on an API response object (line 132), and it correctly handles the test cases for thrown plain objects. The risk is that a non-Feishu error coincidentally carrying code: 99991672 would be misidentified — very unlikely given how specific that code is, but worth a brief comment explaining the dual-path design.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/bot-sender-name.ts
Line: 52

Comment:
**`extractPermissionError` now treats the raw error object as data**

`const data = axiosErr.response?.data ?? err` means any object with `code: 99991672` will now be matched as a Feishu permission error even without an axios `response` wrapper. This is intentional for the non-throw path where `extractPermissionError(res)` is called directly on an API response object (line 132), and it correctly handles the test cases for thrown plain objects. The risk is that a non-Feishu error coincidentally carrying `code: 99991672` would be misidentified — very unlikely given how specific that code is, but worth a brief comment explaining the dual-path design.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +127 to +130
const res: FeishuChatMembersGetResponse = await getChatMembers({
path: { chat_id: normalizedChatId },
params: { page_size: 10, member_id_type: memberIdType },
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No negative-hit caching for direct member lookup

When the API call succeeds but no matching member is found (e.g., matchedMember is undefined or the name trims to empty), the function returns {} without caching anything. This means every subsequent message in the same DM will trigger another chatMembers.get call until a name is resolved. The existing contact (senderNameCache) path has the same omission. For DMs this is limited to 2 members per call and the TTL would cap the problem, but consider storing a sentinel or using the same TTL on negative hits to bound API usage when a user genuinely has no display name configured.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/bot-sender-name.ts
Line: 127-130

Comment:
**No negative-hit caching for direct member lookup**

When the API call succeeds but no matching member is found (e.g., `matchedMember` is undefined or the name trims to empty), the function returns `{}` without caching anything. This means every subsequent message in the same DM will trigger another `chatMembers.get` call until a name is resolved. The existing contact (`senderNameCache`) path has the same omission. For DMs this is limited to 2 members per call and the TTL would cap the problem, but consider storing a sentinel or using the same TTL on negative hits to bound API usage when a user genuinely has no display name configured.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: found issues before merge.

Summary
The PR adds Feishu DM sender-name resolution through chatMembers before contact fallback, account-scoped sender-name caches, permission-guidance propagation, topic-label tests, and a changelog entry.

Reproducibility: yes. A focused unit test can mock chatMembers.get to return a single member whose member_id differs from the inbound senderId; the PR code currently returns that non-matching name instead of falling through safely.

Next step before merge
Human review is required because this protected automerge PR is dirty/unmergeable and has security-sensitive P2 findings, so the next action is maintainer/security follow-up rather than a normal repair queue.

Security
Needs attention: The diff introduces security-sensitive Feishu sender attribution, cache growth, and permission URL handling concerns that should be fixed before merge.

Review findings

  • [P2] Require an exact sender match before using the member name — extensions/feishu/src/bot-sender-name.ts:141-143
  • [P2] Bound the direct-member name cache — extensions/feishu/src/bot-sender-name.ts:31
  • [P2] Validate grant URLs before accepting raw permission responses — extensions/feishu/src/bot-sender-name.ts:52-65
Review details

Best possible solution:

Keep the Feishu-plugin change set, but repair the PR head by requiring exact sender matches, bounding/pruning the new caches, validating Feishu/Lark grant URL hosts, rebasing cleanly, and rerunning focused Feishu tests plus the changed gate.

Do we have a high-confidence way to reproduce the issue?

Yes. A focused unit test can mock chatMembers.get to return a single member whose member_id differs from the inbound senderId; the PR code currently returns that non-matching name instead of falling through safely.

Is this the best way to solve the issue?

No, not as submitted. The product direction is narrow and in the right plugin, but the maintainable implementation needs exact attribution, bounded cache behavior, URL validation, and a clean rebase before merge.

Full review comments:

  • [P2] Require an exact sender match before using the member name — extensions/feishu/src/bot-sender-name.ts:141-143
    When chatMembers.get returns exactly one item but that item's member_id does not match the inbound sender, this fallback still uses that name. That can attribute a DM to the wrong person; drop the single-item fallback and let contact lookup handle misses.
    Confidence: 0.88
  • [P2] Bound the direct-member name cache — extensions/feishu/src/bot-sender-name.ts:31
    directMemberNameCache is process-wide and keyed by account, chat, and sender, but expired entries are never pruned and there is no max size. A long-running gateway that sees many distinct direct chats can grow this map indefinitely.
    Confidence: 0.82
  • [P2] Validate grant URLs before accepting raw permission responses — extensions/feishu/src/bot-sender-name.ts:52-65
    The changed extractor now treats raw response objects as Feishu permission errors while still accepting any https://.../app/... URL. Since that URL is later shown in agent-visible guidance, restrict it to official Feishu/Lark hosts or omit it.
    Confidence: 0.76

Overall correctness: patch is incorrect
Overall confidence: 0.84

Security concerns:

  • [medium] Potential DM sender-name misattribution — extensions/feishu/src/bot-sender-name.ts:141
    The new direct-member fallback can use a single non-matching chat member as the sender name, which is an integrity issue for transcripts and agent context.
    Confidence: 0.88
  • [medium] Unbounded direct-member name cache — extensions/feishu/src/bot-sender-name.ts:31
    The new process-wide direct-member cache stores account/chat/sender entries without max size or expired-entry pruning, allowing avoidable memory growth in long-running gateways.
    Confidence: 0.82
  • [medium] Unvalidated permission grant URL — extensions/feishu/src/bot-sender-name.ts:61
    A URL parsed from Feishu error text is accepted without host validation and later shown in agent-visible system guidance; unexpected hosts should be dropped or restricted to official Feishu/Lark domains.
    Confidence: 0.76

Acceptance criteria:

  • pnpm exec vitest run --config vitest.extensions.config.ts extensions/feishu/src/bot.test.ts extensions/feishu/src/bot-sender-name.test.ts
  • pnpm exec vitest run src/config/sessions.test.ts
  • pnpm check:changed

What I checked:

  • protected-pr-state: The PR is open, authored by a MEMBER, labeled maintainer, clawsweeper:automerge, and clawsweeper:human-review; the PR API reports mergeable_state: dirty, mergeable: false, rebaseable: false, and maintainer_can_modify: false at head bdc3b6ef0e1d. (bdc3b6ef0e1d)
  • current-main-lacks-dm-fallback: Current main's resolveFeishuSenderName only accepts account, senderId, and log, caches by sender id, and calls contact.user.get; it has no chatId/chatType input or direct im.chatMembers.get path. (extensions/feishu/src/bot-sender-name.ts:76, 4a7e60d05bcd)
  • pr-head-adds-dm-member-path: The PR head adds resolveFeishuDirectMemberName, calls client.im.chatMembers.get for p2p/private chats, and passes chatId/chatType from handleFeishuMessage into sender-name resolution. (extensions/feishu/src/bot-sender-name.ts:101, bdc3b6ef0e1d)
  • sender-misattribution-risk: When no returned member id matches the inbound sender id, the PR accepts the sole returned member anyway, which can attribute a DM to the wrong person instead of falling back safely. (extensions/feishu/src/bot-sender-name.ts:141, bdc3b6ef0e1d)
  • unbounded-direct-member-cache: The PR adds a process-wide directMemberNameCache and writes account/chat/sender entries with only an expireAt timestamp; there is no size cap or expired-entry pruning. (extensions/feishu/src/bot-sender-name.ts:31, bdc3b6ef0e1d)
  • unvalidated-permission-url: The PR broadens permission-error parsing to raw response objects while still accepting the first https://.../app/... URL without host validation; buildFeishuAgentBody later places that URL into agent-visible system guidance. (extensions/feishu/src/bot-sender-name.ts:52, bdc3b6ef0e1d)

Likely related people:

  • steipete: GitHub commit history shows the current bot-sender-name.ts helper was split out by 005b25e9d42a, with later Feishu topic/session and cleanup work in the same area. (role: current Feishu sender-name and topic-session maintainer; confidence: high; commits: 005b25e9d42a, bb5e278f63bd, 2187b19d7eb7; files: extensions/feishu/src/bot-sender-name.ts, extensions/feishu/src/bot.ts)
  • jnuyao: Merged PR feat(feishu): display group name instead of chat_id in session labels #51032 landed commit 2a08848dd137, adding Feishu group-name session label behavior that this PR intentionally keeps as the baseline. (role: introduced merged group-name baseline; confidence: high; commits: 2a08848dd137; files: extensions/feishu/src/bot.ts, extensions/feishu/src/bot-group-name.test.ts)
  • vincentkoc: Separate from authoring this PR, prior merged history shows multiple recent Feishu bot.ts runtime, thread-context, reasoning-preview, and message/mention changes. (role: recent adjacent Feishu maintainer; confidence: medium; commits: 2b96f53f9782, d609f71c9b74, beb108cfaa77; files: extensions/feishu/src/bot.ts)
  • futuremind2026: Closed unmerged PR feat(feishu): configurable group and DM display-name resolution #38958 is explicitly credited as the source direction for the DM fallback, cache-scoping, permission-guidance, and topic-label work carried forward here. (role: source implementation proposer; confidence: medium; commits: b4ec2858589a; files: extensions/feishu/src/bot.ts, src/config/sessions/group.ts, src/config/sessions.test.ts)

Remaining risk / open question:

  • The branch is dirty against its base and maintainer_can_modify is false, so any repair/rebase path may need maintainer or author action before automerge can resume.
  • The exact official Feishu/Lark grant URL allowlist should be confirmed before surfacing permission URLs in agent-visible guidance.
  • The successful CI/check runs on the head predate the current dirty merge state, so they are not sufficient landing proof.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4a7e60d05bcd.

@vincentkoc
Copy link
Copy Markdown
Member Author

/clawsweeper automerge

@clawsweeper clawsweeper Bot added the clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge label May 1, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 1, 2026

🦞🦞
ClawSweeper automerge is enabled.

  • Head: bdc3b6ef0e1d
  • Label: clawsweeper:automerge
  • Action: exact-head review queued.
  • Flow: review this head, repair/rebase only if needed, then re-review the exact repaired head before merge.

Draft PRs stay fix-only until GitHub marks them ready for review. Pause with /clawsweeper stop.

Automerge progress:

  • 2026-05-02 19:37:41 UTC review queued [`bdc3b6ef0e1d`](https://github.com/openclaw/openclaw/commit/bdc3b6ef0e1d34922f4d924c94399fe4ef58a452) (queued)
  • 2026-05-01 12:05:43 UTC repair queued `bdc3b6ef0e1d` (autonomous) Run: https://github.com/openclaw/clawsweeper/actions/runs/25213671283
  • 2026-05-02 19:33:30 UTC review queued [`bdc3b6ef0e1d`](https://github.com/openclaw/openclaw/commit/bdc3b6ef0e1d34922f4d924c94399fe4ef58a452) (queued)

@clawsweeper clawsweeper Bot added the clawsweeper:human-review Needs maintainer review before ClawSweeper can continue label May 2, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 2, 2026

🦞🦞
ClawSweeper is pausing this repair loop for human review.

Source: clawsweeper[bot]
Reason: structured ClawSweeper verdict: needs-human (sha=bdc3b6ef0e1d34922f4d924c94399fe4ef58a452)

I added clawsweeper:human-review and left the final call with a maintainer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge clawsweeper:human-review Needs maintainer review before ClawSweeper can continue clawsweeper Tracked by ClawSweeper automation maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant