mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(matrix): guard malformed poll answers instead of throwing TypeError (#123231)
* fix(matrix): guard malformed poll answers instead of throwing TypeError parsePollStart mapped poll.answers with no runtime shape check, but poll event content is sender-controlled JSON. A poll start event with answers not an array, null entries, or non-string ids threw a TypeError that bubbled through thread/reply context building to the ingress top-level catch, silently dropping the whole message - and every later message replying to or threading on that poll. Drop malformed entries (and return null when nothing valid remains) instead of throwing. * fix(matrix): normalize malformed poll text Signed-off-by: sallyom <somalley@redhat.com> * fix(matrix): use guarded poll records Signed-off-by: sallyom <somalley@redhat.com> --------- Signed-off-by: sallyom <somalley@redhat.com> Co-authored-by: sallyom <somalley@redhat.com>
This commit is contained in:
@@ -67,6 +67,40 @@ describe("parsePollStartContent", () => {
|
||||
|
||||
expect(parsed?.maxSelections).toBe(2);
|
||||
});
|
||||
|
||||
it("drops malformed answers instead of throwing on sender-controlled content", () => {
|
||||
const malformed: Array<[string, unknown]> = [
|
||||
["answers is a string", { question: { "m.text": "Q?" }, answers: "nope" }],
|
||||
["answers entries null", { question: { "m.text": "Q?" }, answers: [null] }],
|
||||
[
|
||||
"answer id non-string",
|
||||
{ question: { "m.text": "Q?" }, answers: [{ id: 42, "m.text": "a" }] },
|
||||
],
|
||||
[
|
||||
"question text non-string",
|
||||
{ question: { "m.text": 42 }, answers: [{ id: "a1", "m.text": "a" }] },
|
||||
],
|
||||
[
|
||||
"answer text non-string",
|
||||
{ question: { "m.text": "Q?" }, answers: [{ id: "a1", "m.text": 42 }] },
|
||||
],
|
||||
];
|
||||
for (const [label, poll] of malformed) {
|
||||
expect(() => parsePollStart({ "m.poll.start": poll } as never), label).not.toThrow();
|
||||
expect(parsePollStart({ "m.poll.start": poll } as never), label).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps well-formed answers when other entries are malformed", () => {
|
||||
const parsed = parsePollStart({
|
||||
"m.poll.start": {
|
||||
question: { "m.text": "Lunch?" },
|
||||
answers: [null, { id: "a1", "m.text": "Yes" }, { id: 42, "m.text": "dropped" }],
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(parsed?.answers).toEqual([{ id: "a1", text: "Yes" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPollStartContent", () => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { normalizePollInput, type PollInput } from "openclaw/plugin-sdk/poll-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
export const M_POLL_START = "m.poll.start" as const;
|
||||
const M_POLL_RESPONSE = "m.poll.response" as const;
|
||||
@@ -120,11 +120,12 @@ export function isPollEventType(eventType: string): boolean {
|
||||
return (POLL_EVENT_TYPES as readonly string[]).includes(eventType);
|
||||
}
|
||||
|
||||
function getTextContent(text?: TextContent): string {
|
||||
if (!text) {
|
||||
function getTextContent(text?: unknown): string {
|
||||
if (!isRecord(text)) {
|
||||
return "";
|
||||
}
|
||||
return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
|
||||
const value = text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body;
|
||||
return normalizeOptionalString(value) ?? "";
|
||||
}
|
||||
|
||||
export function parsePollStart(content: PollStartContent): ParsedPollStart | null {
|
||||
@@ -136,15 +137,18 @@ export function parsePollStart(content: PollStartContent): ParsedPollStart | nul
|
||||
return null;
|
||||
}
|
||||
|
||||
const question = getTextContent(poll.question).trim();
|
||||
const question = getTextContent(poll.question);
|
||||
if (!question) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const answers = poll.answers
|
||||
// Sender-controlled event content can violate declared Matrix types; discard
|
||||
// malformed answers here so context building never drops the whole message.
|
||||
const rawAnswers: unknown = poll.answers;
|
||||
const answers = (Array.isArray(rawAnswers) ? rawAnswers : [])
|
||||
.map((answer) => ({
|
||||
id: answer.id,
|
||||
text: getTextContent(answer).trim(),
|
||||
id: isRecord(answer) && typeof answer.id === "string" ? answer.id : "",
|
||||
text: getTextContent(answer),
|
||||
}))
|
||||
.filter((answer) => answer.id.trim().length > 0 && answer.text.length > 0);
|
||||
if (answers.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user