mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(gateway): parse image data URLs linearly (#102825)
Co-authored-by: WangYan <wang.yan29@xydigit.com>
This commit is contained in:
parent
8985598302
commit
79d3daa09b
2 changed files with 76 additions and 5 deletions
|
|
@ -586,6 +586,59 @@ describe("createManagedOutgoingImageBlocks", () => {
|
||||||
await expectPathMissing(path.join(stateDir, "media", "outgoing", "records"));
|
await expectPathMissing(path.join(stateDir, "media", "outgoing", "records"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects semicolon-heavy malformed data urls without backtracking", async () => {
|
||||||
|
const semicolons = ";".repeat(64);
|
||||||
|
const malformed = `data:image/png${semicolons}`;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
createManagedOutgoingImageBlocks({
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
mediaUrls: [malformed],
|
||||||
|
stateDir,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Invalid image data URL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed data urls with a later ;base64, marker after a payload comma", async () => {
|
||||||
|
await expect(
|
||||||
|
createManagedOutgoingImageBlocks({
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
mediaUrls: ["data:image/png;base64,garbage;base64,iVBORw0KGgo="],
|
||||||
|
stateDir,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Invalid image data URL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires the base64 marker to be adjacent to the payload comma", async () => {
|
||||||
|
await expect(
|
||||||
|
createManagedOutgoingImageBlocks({
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
mediaUrls: [`data:image/png;base64\n,${TINY_PNG_BASE64}`],
|
||||||
|
stateDir,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Invalid image data URL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves parameterized image data urls", async () => {
|
||||||
|
const blocks = await createManagedOutgoingImageBlocks({
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
mediaUrls: [`data:image/png;charset=utf-8;base64,${TINY_PNG_BASE64}`],
|
||||||
|
stateDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requireBlock(blocks).mimeType).toBe("image/png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects data urls without a media type", async () => {
|
||||||
|
await expect(
|
||||||
|
createManagedOutgoingImageBlocks({
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
mediaUrls: [`data:;base64,${TINY_PNG_BASE64}`],
|
||||||
|
stateDir,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Invalid image data URL");
|
||||||
|
});
|
||||||
|
|
||||||
it("rewrites local image sources into managed display blocks without leaking the source path", async () => {
|
it("rewrites local image sources into managed display blocks without leaking the source path", async () => {
|
||||||
const sourcePath = path.join(stateDir, "workspace", "fixtures", "dot.png");
|
const sourcePath = path.join(stateDir, "workspace", "fixtures", "dot.png");
|
||||||
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
||||||
|
|
|
||||||
|
|
@ -279,22 +279,40 @@ function parseImageDataUrl(
|
||||||
if (!trimmed.startsWith("data:")) {
|
if (!trimmed.startsWith("data:")) {
|
||||||
return { kind: "not-data-url" };
|
return { kind: "not-data-url" };
|
||||||
}
|
}
|
||||||
const match = /^data:([^;,]+)(?:;[^,]*)*;base64,([A-Za-z0-9+/=\s]+)$/i.exec(trimmed);
|
|
||||||
if (!match) {
|
const afterPrefix = trimmed.slice("data:".length);
|
||||||
|
const commaIdx = afterPrefix.indexOf(",");
|
||||||
|
const mimeAndParams = commaIdx < 0 ? "" : afterPrefix.slice(0, commaIdx);
|
||||||
|
const base64Marker = ";base64";
|
||||||
|
if (mimeAndParams.slice(-base64Marker.length).toLowerCase() !== base64Marker) {
|
||||||
throw new Error("Invalid image data URL");
|
throw new Error("Invalid image data URL");
|
||||||
}
|
}
|
||||||
const contentType = match[1]?.trim().toLowerCase() ?? "";
|
const semicolonIdx = mimeAndParams.indexOf(";");
|
||||||
|
const contentType = (semicolonIdx < 0 ? mimeAndParams : mimeAndParams.slice(0, semicolonIdx))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!contentType) {
|
||||||
|
throw new Error("Invalid image data URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64Part = afterPrefix.slice(commaIdx + 1);
|
||||||
|
if (!/^[A-Za-z0-9+/=\s]+$/.test(base64Part)) {
|
||||||
|
throw new Error("Invalid image data URL");
|
||||||
|
}
|
||||||
|
|
||||||
if (!contentType.startsWith("image/")) {
|
if (!contentType.startsWith("image/")) {
|
||||||
return { kind: "non-image-data-url" };
|
return { kind: "non-image-data-url" };
|
||||||
}
|
}
|
||||||
if (estimateBase64DecodedByteLength(match[2]) > limits.maxBytes) {
|
|
||||||
|
if (estimateBase64DecodedByteLength(base64Part) > limits.maxBytes) {
|
||||||
throw createManagedImageAttachmentError(
|
throw createManagedImageAttachmentError(
|
||||||
`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`,
|
`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "image-data-url",
|
kind: "image-data-url",
|
||||||
buffer: Buffer.from(match[2].replace(/\s+/g, ""), "base64"),
|
buffer: Buffer.from(base64Part.replace(/\s+/g, ""), "base64"),
|
||||||
contentType,
|
contentType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue