revert(agents): revert base64 image validation (#19221)

This commit is contained in:
Seb Slight
2026-02-17 09:58:39 -05:00
committed by GitHub
parent bd1e7fadd5
commit 4536a6e05f
2 changed files with 2 additions and 203 deletions

View File

@@ -2,153 +2,6 @@ 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 padding in wrong position", async () => {
const blocks = [
{
type: "image" as const,
data: "SGVs=bG8=", // = in middle is invalid
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("normalizes URL-safe base64 to standard 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();
// Convert to URL-safe base64 (replace + with -, / with _)
const standardBase64 = jpeg.toString("base64");
const urlSafeBase64 = standardBase64.replace(/\+/g, "-").replace(/\//g, "_");
const blocks = [
{
type: "image" as const,
data: urlSafeBase64,
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 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;

View File

@@ -17,55 +17,6 @@ const MAX_IMAGE_DIMENSION_PX = 2000;
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
const log = createSubsystemLogger("agents/tool-images");
// Valid base64: alphanumeric, +, /, with 0-2 trailing = padding only
// This regex ensures = only appears at the end as valid padding
const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/;
/**
* Validates and normalizes base64 image data before processing.
* - Strips data URL prefixes (e.g., "data:image/png;base64,")
* - Converts URL-safe base64 to standard base64 (- → +, _ → /)
* - Validates base64 character set and structure
* - Ensures the string is not empty after trimming
*
* Returns the cleaned base64 string or throws an error if invalid.
*/
function validateAndNormalizeBase64(base64: string): string {
let data = base64.trim();
// Strip data URL prefix if present (e.g., "data:image/png;base64,...")
const dataUrlMatch = data.match(/^data:[^;]+;base64,(.*)$/i);
if (dataUrlMatch) {
data = dataUrlMatch[1].trim();
}
if (!data) {
throw new Error("Base64 data is empty");
}
// Normalize URL-safe base64 to standard base64
// URL-safe uses - instead of + and _ instead of /
data = data.replace(/-/g, "+").replace(/_/g, "/");
// Check for valid base64 characters and structure
// The regex ensures = only appears as 0-2 trailing padding chars
// Node's Buffer.from silently ignores invalid chars, but Anthropic API rejects them
if (!BASE64_REGEX.test(data)) {
throw new Error("Base64 data contains invalid characters or malformed padding");
}
// Check that length is valid for base64 (must be multiple of 4 when padded)
// Remove padding for length check, then verify
const withoutPadding = data.replace(/=+$/, "");
const remainder = withoutPadding.length % 4;
if (remainder === 1) {
// A single char remainder is always invalid in base64
throw new Error("Base64 data has invalid length");
}
return data;
}
function isImageBlock(block: unknown): block is ImageContentBlock {
if (!block || typeof block !== "object") {
return false;
@@ -209,8 +160,8 @@ export async function sanitizeContentBlocksImages(
continue;
}
const rawData = block.data.trim();
if (!rawData) {
const data = block.data.trim();
if (!data) {
out.push({
type: "text",
text: `[${label}] omitted empty image payload`,
@@ -219,11 +170,6 @@ export async function sanitizeContentBlocksImages(
}
try {
// Validate and normalize base64 before processing
// This catches invalid base64 that Buffer.from() would silently accept
// but Anthropic's API would reject, preventing permanent session corruption
const data = validateAndNormalizeBase64(rawData);
const inferredMimeType = inferMimeTypeFromBase64(data);
const mimeType = inferredMimeType ?? block.mimeType;
const resized = await resizeImageBase64IfNeeded({