fix(mattermost): keep slash command error truncation UTF-16 safe (#102607)

* fix(mattermost): keep slash command error truncation UTF-16 safe

Replace `.slice(0, 300)` and `.slice(0, 200)` with `truncateUtf16Safe`
in sanitizeCommandLookupError and sanitizeMattermostLogValue to prevent
surrogate pair corruption.

Ref. lsr911 pattern — mechanical substitution, no behavior change.

* test(mattermost): cover UTF-16-safe slash logs

Co-authored-by: 黄剑雄0668001315 <huang.jianxiong@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
huangjianxiong 2026-07-09 19:20:38 +08:00 committed by GitHub
parent fb52a989b2
commit ac71074807
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 8 deletions

View file

@ -713,8 +713,9 @@ describe("slash-http", () => {
it("logs when command lookup by id returns a deleted command before fallback", async () => {
const registeredCommand = createRegisteredCommand();
const commandId = `${"i".repeat(199)}😀tail`;
const command = {
id: "cmd-1\r\nspoofed",
id: commandId,
token: "valid-token",
team_id: "t1",
trigger: "oc_status",
@ -748,9 +749,9 @@ describe("slash-http", () => {
expect(log).toHaveBeenCalledTimes(1);
const message = firstLogMessage(log);
expect(message).not.toMatch(/[\r\n\t]/u);
expect(message).toContain("deleted command cmd-1 spoofed");
expect(message).toContain("using team list fallback");
expect(message).toBe(
`mattermost: slash command lookup by id returned deleted command ${"i".repeat(199)} for /oc_status; using team list fallback`,
);
});
it("rejects current commands with a mismatched method or callback URL", async () => {
@ -923,4 +924,35 @@ describe("slash-http", () => {
expect(message).not.toContain("secret-query");
expect(message).not.toContain("user:pass");
});
it("keeps upstream lookup error previews UTF-16 safe", async () => {
const registeredCommand = createRegisteredCommand();
const client = createCommandLookupClient({
commandLookupError: new Error("primary failure"),
listLookupError: new Error(`${"e".repeat(299)}😀tail`),
});
const log = vi.fn();
await expect(
validateMattermostSlashCommandToken({
accountId: "default",
client,
registeredCommand,
payload: {
token: "valid-token",
team_id: "t1",
channel_id: "c1",
user_id: "u1",
command: "/oc_status",
text: "",
},
log,
}),
).resolves.toBe(false);
expect(log).toHaveBeenCalledTimes(1);
expect(firstLogMessage(log)).toBe(
`mattermost: slash command registration check failed for /oc_status: ${"e".repeat(299)}; command lookup: primary failure`,
);
});
});

View file

@ -12,6 +12,7 @@ import {
} from "openclaw/plugin-sdk/number-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
import { getMattermostRuntime } from "../runtime.js";
import {
@ -142,7 +143,7 @@ function isDeletedMattermostCommand(command: { delete_at?: number }): boolean {
function sanitizeCommandLookupError(error: unknown): string {
const raw = error instanceof Error ? error.message : String(error);
return raw
const sanitized = raw
.replace(/[\r\n\t]/gu, " ")
.replace(/https?:\/\/[^\s)\]}]+/giu, (urlText) => {
try {
@ -165,12 +166,12 @@ function sanitizeCommandLookupError(error: unknown): string {
.replace(
/\b(token|authorization|access_token|refresh_token|client_secret|botToken)\b(\s*["']?\s*(?:=|:)\s*["']?)[^"',\s;}]+/giu,
"$1$2[redacted]",
)
.slice(0, 300);
);
return truncateUtf16Safe(sanitized, 300);
}
function sanitizeMattermostLogValue(value: string): string {
return value.replace(/[\r\n\t]/gu, " ").slice(0, 200);
return truncateUtf16Safe(value.replace(/[\r\n\t]/gu, " "), 200);
}
async function withCommandLookupTimeout<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T> {