fix(openai): reject malformed Codex image base64 (#111235)

* fix(openai): reject malformed Codex image base64

* fix(openai): align Codex image base64 trimming

* fix(openai): match Codex base64 decoding
This commit is contained in:
Wynne668
2026-07-19 20:40:18 -04:00
committed by GitHub
parent 3c0f55b58f
commit 8a858c9c65
2 changed files with 91 additions and 2 deletions
@@ -1629,6 +1629,79 @@ describe("openai image generation provider", () => {
});
});
it.each([
{
name: "invalid alphabet from output item",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "aGVs!bG8=" },
},
},
{
name: "invalid alphabet from completed output",
event: {
type: "response.completed",
response: {
output: [{ type: "image_generation_call", result: "aGVs!bG8=" }],
},
},
},
{
name: "missing canonical padding",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "aGVsbG8" },
},
},
{
name: "internal whitespace",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "aGVs bG8=" },
},
},
{
name: "non-zero trailing bits",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "Zh==" },
},
},
])("rejects malformed Codex image base64 from $name events", async ({ event }) => {
mockCodexAuthOnly();
mockCodexRawStream(`data: ${JSON.stringify(event)}\n\n`);
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw from malformed image data",
cfg: {},
}),
).rejects.toThrow("OpenAI Codex image generation returned malformed base64 image data");
});
it("accepts Codex-compatible surrounding Unicode whitespace", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
`data: ${JSON.stringify({
type: "response.output_item.done",
item: { type: "image_generation_call", result: "\u0085aGVsbG8=\u0085" },
})}\n\n`,
);
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw from valid image data",
cfg: {},
});
expect(result.images[0]?.buffer).toEqual(Buffer.from("hello"));
});
it("honors configured Codex transport overrides for OAuth image generation", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
+18 -2
View File
@@ -14,7 +14,7 @@ import {
import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core";
import { resolveClosestSize } from "openclaw/plugin-sdk/media-generation-runtime";
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
import { MAX_IMAGE_BYTES } from "openclaw/plugin-sdk/media-runtime";
import { canonicalizeBase64, MAX_IMAGE_BYTES } from "openclaw/plugin-sdk/media-runtime";
import {
ensureAuthProfileStore,
hasConfiguredSecretInput,
@@ -66,6 +66,7 @@ const OPENAI_MAX_IMAGE_RESULTS = 4;
const MAX_CODEX_IMAGE_SSE_BYTES = 64 * 1024 * 1024;
const MAX_CODEX_IMAGE_SSE_EVENTS = 512;
const MAX_CODEX_IMAGE_BASE64_CHARS = 64 * 1024 * 1024;
const STANDARD_BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const LOG_VALUE_MAX_CHARS = 256;
const MOCK_OPENAI_PROVIDER_ID = "mock-openai";
const OPENAI_OUTPUT_FORMATS = ["png", "jpeg", "webp"] as const;
@@ -577,7 +578,22 @@ function decodeCodexImagePayload(payload: string): Buffer {
if (payload.length > MAX_CODEX_IMAGE_BASE64_CHARS) {
throw new Error("OpenAI Codex image generation result exceeded size limit");
}
return Buffer.from(payload, "base64");
// Rust's str::trim follows Unicode White_Space (including U+0085), while
// JavaScript's trim does not. Match Codex before enforcing canonical Base64.
const trimmedPayload = payload.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "");
const canonicalPayload = canonicalizeBase64(trimmedPayload);
const padding = canonicalPayload?.endsWith("==") ? 2 : canonicalPayload?.endsWith("=") ? 1 : 0;
const trailingBitsMask = padding === 2 ? 0x0f : padding === 1 ? 0x03 : 0;
const trailingValue =
padding > 0 ? STANDARD_BASE64_ALPHABET.indexOf(canonicalPayload?.at(-(padding + 1)) ?? "") : 0;
if (
!canonicalPayload ||
canonicalPayload !== trimmedPayload ||
(trailingValue & trailingBitsMask) !== 0
) {
throw new Error("OpenAI Codex image generation returned malformed base64 image data");
}
return Buffer.from(canonicalPayload, "base64");
}
function toCodexImage(