mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(agents): bound base64 image input before decode in tool-image sanitizer (#105086)
* fix(agents): bound base64 image input before decode in tool-image sanitizer
* fix(agents): lower input-size cap to 10MB for OOM headroom
* fix(agents): align tool-image input-cap comment with 10 MiB ceiling
* fix(agents): typecheck-safe access in tool-image input-cap test
* test(agents): exercise real tool image input cap
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit b85531c62f)
This commit is contained in:
committed by
Dallin Romney
parent
028fc97809
commit
5400d5bedf
@@ -0,0 +1,25 @@
|
||||
// Tool image input-cap tests verify pathological oversized base64 input is
|
||||
// rejected before Buffer.from allocates a transient multi-MB buffer.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sanitizeContentBlocksImages } from "./tool-images.js";
|
||||
|
||||
const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
describe("tool image sanitizer oversized input cap", () => {
|
||||
it("rejects oversized estimated input before decode allocation", async () => {
|
||||
const encodedLength = Math.ceil(((MAX_IMAGE_INPUT_BYTES + 1) * 4) / 3 / 4) * 4;
|
||||
const oversizedBase64 = "A".repeat(encodedLength);
|
||||
|
||||
const out = await sanitizeContentBlocksImages(
|
||||
[{ type: "image" as const, data: oversizedBase64, mimeType: "image/png" }],
|
||||
"test",
|
||||
);
|
||||
|
||||
expect(out).toStrictEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: "[test] omitted image payload: image exceeds input size limit (10.00MB)",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Downscales and recompresses oversized base64 image blocks before provider replay.
|
||||
*/
|
||||
import { canonicalizeBase64 } from "@openclaw/media-core/base64";
|
||||
import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64";
|
||||
import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion";
|
||||
import { toErrorObject } from "../infra/errors.js";
|
||||
import type { ImageContent } from "../llm/types.js";
|
||||
@@ -33,6 +33,11 @@ type TextContentBlock = Extract<ToolContentBlock, { type: "text" }>;
|
||||
// tool outputs do not break later turns or silent channel replies.
|
||||
const MAX_IMAGE_DIMENSION_PX = DEFAULT_IMAGE_MAX_DIMENSION_PX;
|
||||
const MAX_IMAGE_BYTES = DEFAULT_IMAGE_MAX_BYTES;
|
||||
// Hard cap on decoded input bytes before Buffer.from/resizer allocation. A
|
||||
// conservative limit well below demonstrated OOM thresholds, leaving headroom
|
||||
// for canonicalization, decode, and image-processing allocations while still
|
||||
// permitting legitimate tool-output images.
|
||||
const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024;
|
||||
const log = createSubsystemLogger("agents/tool-images");
|
||||
|
||||
function isImageTypeBlock(block: unknown): block is Record<string, unknown> & { type: "image" } {
|
||||
@@ -327,6 +332,19 @@ export async function sanitizeContentBlocksImages(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Estimate decoded bytes on the raw payload before trim/canonicalize/decode
|
||||
// so pathological multi-GB base64 cannot force a transient large allocation.
|
||||
// maxBytes is the post-decode resize target; MAX_IMAGE_INPUT_BYTES is a
|
||||
// conservative pre-decode ceiling (10 MiB) far below the 25MP/100MB
|
||||
// processing headroom, so legitimate tool images still decode and resize.
|
||||
if (estimateBase64DecodedBytes(block.data) > MAX_IMAGE_INPUT_BYTES) {
|
||||
out.push({
|
||||
type: "text",
|
||||
text: `[${label}] omitted image payload: image exceeds input size limit (${formatBytesShort(MAX_IMAGE_INPUT_BYTES)})`,
|
||||
} satisfies TextContentBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = block.data.trim();
|
||||
if (!data) {
|
||||
out.push({
|
||||
|
||||
Reference in New Issue
Block a user