mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
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:
@@ -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" });
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user