












@@ -2,6 +2,7 @@ import path from "node:path";
22import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
33import { openLocalFileSafely } from "../../infra/fs-safe.js";
44import { assertNoWindowsNetworkPath, safeFileURLToPath } from "../../infra/local-file-access.js";
5+import { estimateBase64DecodedBytes } from "../../media/base64.js";
56import { assertLocalMediaAllowed, LocalMediaAccessError } from "../../media/local-media-access.js";
67import { isAudioFileName } from "../../media/mime.js";
78import { resolveSendableOutboundReplyParts } from "../../plugin-sdk/reply-payload.js";
@@ -131,10 +132,31 @@ function mimeTypeForPath(filePath: string): string {
131132return MIME_BY_EXT[ext] ?? "audio/mpeg";
132133}
133134134-function estimateBase64DecodedBytes(base64: string): number {
135-const sanitized = base64.replace(/\s+/g, "");
136-const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
137-return Math.floor((sanitized.length * 3) / 4) - padding;
135+function isBase64DataPayload(value: string): boolean {
136+if (value.length === 0) {
137+return false;
138+}
139+for (let index = 0; index < value.length; index += 1) {
140+const code = value.charCodeAt(index);
141+const isBase64Char =
142+(code >= 0x41 && code <= 0x5a) ||
143+(code >= 0x61 && code <= 0x7a) ||
144+(code >= 0x30 && code <= 0x39) ||
145+code === 0x2b ||
146+code === 0x2f ||
147+code === 0x3d;
148+const isWhitespace =
149+code === 0x09 ||
150+code === 0x0a ||
151+code === 0x0b ||
152+code === 0x0c ||
153+code === 0x0d ||
154+code === 0x20;
155+if (!isBase64Char && !isWhitespace) {
156+return false;
157+}
158+}
159+return true;
138160}
139161140162function resolveEmbeddableImageUrl(url: string): string | null {
@@ -145,12 +167,17 @@ function resolveEmbeddableImageUrl(url: string): string | null {
145167if (trimmed.length > MAX_WEBCHAT_IMAGE_DATA_URL_CHARS) {
146168return null;
147169}
148-const match = /^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]+)$/i.exec(trimmed);
149-if (!match) {
170+const commaIndex = trimmed.indexOf(",");
171+if (commaIndex < 0) {
172+return null;
173+}
174+const metadata = trimmed.slice(0, commaIndex);
175+const match = /^data:(image\/[a-z0-9.+-]+);base64$/i.exec(metadata);
176+const base64Data = trimmed.slice(commaIndex + 1);
177+if (!match || !isBase64DataPayload(base64Data)) {
150178return null;
151179}
152180const mediaType = normalizeLowercaseStringOrEmpty(match[1]);
153-const base64Data = match[2];
154181if (!ALLOWED_WEBCHAT_DATA_IMAGE_MEDIA_TYPES.has(mediaType)) {
155182return null;
156183}
此內容由慣性聚合(RSS閱讀器)自動聚合整理,僅供閱讀參考。 原文來自 — 版權歸原作者所有。