Skip to content

Commit f3f6a86

Browse files
committed
fix(msteams): validate inline image base64
1 parent 6ae9c8b commit f3f6a86

3 files changed

Lines changed: 46 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai
4040
- Debug proxy: reject malformed relative-form proxy targets with a controlled 400 response instead of letting URL parsing escape the request handler.
4141
- File transfer: reject malformed inline `file_write` base64 before computing hashes or invoking paired nodes, avoiding Node's lenient base64 decoder.
4242
- QA channel: skip malformed inline inbound attachment base64 instead of staging silently corrupted media for agent turns.
43+
- Microsoft Teams: reject malformed inline HTML image base64 padding instead of decoding corrupted `data:` image attachments.
4344
- Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom.
4445
- Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd.
4546
- CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf.

extensions/msteams/src/attachments/shared.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,17 @@ describe("msteams inline image limits", () => {
515515
}
516516
});
517517

518+
it("rejects inline data images with malformed base64 padding", () => {
519+
const attachments = [
520+
{
521+
contentType: "text/html",
522+
content: `<img src="data:image/png;base64,aGV=sbG8=" />`,
523+
},
524+
];
525+
const out = extractInlineImageCandidates(attachments, { maxInlineBytes: 10 });
526+
expect(out).toStrictEqual([]);
527+
});
528+
518529
it("enforces cumulative inline size limit across attachments", () => {
519530
const attachments = [
520531
{

extensions/msteams/src/attachments/shared.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,36 @@ export function extractHtmlFromAttachment(att: MSTeamsAttachmentLike): string |
307307
return text;
308308
}
309309

310-
function isLikelyBase64Payload(value: string): boolean {
311-
return /^[A-Za-z0-9+/=\r\n]+$/.test(value);
310+
function canonicalizeInlineBase64Payload(value: string): string | undefined {
311+
let cleaned = "";
312+
let padding = 0;
313+
let sawPadding = false;
314+
for (let index = 0; index < value.length; index += 1) {
315+
const code = value.charCodeAt(index);
316+
if (code <= 0x20) {
317+
continue;
318+
}
319+
if (code === 0x3d) {
320+
padding += 1;
321+
if (padding > 2) {
322+
return undefined;
323+
}
324+
sawPadding = true;
325+
cleaned += "=";
326+
continue;
327+
}
328+
const isDataChar =
329+
(code >= 0x41 && code <= 0x5a) ||
330+
(code >= 0x61 && code <= 0x7a) ||
331+
(code >= 0x30 && code <= 0x39) ||
332+
code === 0x2b ||
333+
code === 0x2f;
334+
if (sawPadding || !isDataChar) {
335+
return undefined;
336+
}
337+
cleaned += value[index];
338+
}
339+
return cleaned && cleaned.length % 4 === 0 ? cleaned : undefined;
312340
}
313341

314342
function decodeDataImageWithLimits(
@@ -325,11 +353,12 @@ function decodeDataImageWithLimits(
325353
return { candidate: null, estimatedBytes: 0 };
326354
}
327355
const payload = match[3] ?? "";
328-
if (!payload || !isLikelyBase64Payload(payload)) {
356+
const canonicalPayload = canonicalizeInlineBase64Payload(payload);
357+
if (!canonicalPayload) {
329358
return { candidate: null, estimatedBytes: 0 };
330359
}
331360

332-
const estimatedBytes = estimateBase64DecodedBytes(payload);
361+
const estimatedBytes = estimateBase64DecodedBytes(canonicalPayload);
333362
if (estimatedBytes <= 0) {
334363
return { candidate: null, estimatedBytes: 0 };
335364
}
@@ -338,7 +367,7 @@ function decodeDataImageWithLimits(
338367
}
339368

340369
try {
341-
const data = Buffer.from(payload, "base64");
370+
const data = Buffer.from(canonicalPayload, "base64");
342371
return {
343372
candidate: { kind: "data", data, contentType, placeholder: "<media:image>" },
344373
estimatedBytes,

0 commit comments

Comments
 (0)