fix: validate base64 image data before API submission

Adds explicit base64 format validation in sanitizeContentBlocksImages()
to prevent invalid image data from being sent to the Anthropic API.

The Problem:
- Node's Buffer.from(str, "base64") silently ignores invalid characters
- Invalid base64 passes local validation but fails at Anthropic's stricter API
- Once corrupted data persists in session history, every API call fails

The Fix:
- Add validateAndNormalizeBase64() function that:
  - Strips data URL prefixes (e.g., "data:image/png;base64,...")
  - Validates base64 character set with regex
  - Checks for valid padding (0-2 '=' chars)
  - Validates length is proper for base64 encoding
- Invalid images are replaced with descriptive text blocks
- Prevents permanent session corruption

Tests:
- Rejects invalid base64 characters
- Strips data URL prefixes correctly
- Rejects invalid padding
- Rejects invalid length
- Handles empty data gracefully

Closes #18212

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sriram Naidu Thota
2026-02-16 12:08:38 -05:00
committed by Peter Steinberger
parent aeec95f870
commit 38c96bc53e
2 changed files with 155 additions and 2 deletions

View File

@@ -2,6 +2,106 @@ import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { sanitizeContentBlocksImages, sanitizeImageBlocks } from "./tool-images.js";
describe("base64 validation", () => {
it("rejects invalid base64 characters and replaces with error text", async () => {
const blocks = [
{
type: "image" as const,
data: "not-valid-base64!!!@#$%",
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
expect(out[0].text).toContain("invalid");
}
});
it("strips data URL prefix and processes valid base64", async () => {
// Create a small valid image
const jpeg = await sharp({
create: {
width: 10,
height: 10,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.jpeg()
.toBuffer();
const base64 = jpeg.toString("base64");
const dataUrl = `data:image/jpeg;base64,${base64}`;
const blocks = [
{
type: "image" as const,
data: dataUrl,
mimeType: "image/jpeg",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("image");
});
it("rejects base64 with invalid padding", async () => {
const blocks = [
{
type: "image" as const,
data: "SGVsbG8===", // too many padding chars
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
}
});
it("rejects base64 with invalid length", async () => {
const blocks = [
{
type: "image" as const,
data: "AAAAA", // length 5 without padding is invalid (remainder 1)
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
}
});
it("handles empty base64 data gracefully", async () => {
const blocks = [
{
type: "image" as const,
data: " ",
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted empty image payload");
}
});
});
describe("tool image sanitizing", () => {
it("shrinks oversized images to <=5MB", async () => {
const width = 2800;