mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(nodes): reject malformed media payloads when base64 is invalid (#104414)
* fix(nodes): reject malformed media base64 payloads * fix(nodes): keep media base64 size preflight * refactor(nodes): reuse shared base64 boundary Co-authored-by: qingminlong <qing.minlong@xydigit.com> * docs(changelog): note node media validation * chore: leave changelog to release automation * test(nodes): cover empty invalid media payloads --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
58ef98ec13
commit
183139f3ed
@@ -223,10 +223,10 @@ describe("nodes camera helpers", () => {
|
||||
).rejects.toThrow(/node remoteip/i);
|
||||
});
|
||||
|
||||
it("writes base64 to file", async () => {
|
||||
it("normalizes valid base64 before writing", async () => {
|
||||
await withCameraTempDir(async (dir) => {
|
||||
const out = path.join(dir, "x.bin");
|
||||
await writeBase64ToFile(out, "aGk=");
|
||||
await writeBase64ToFile(out, " aGk\n");
|
||||
await expect(readFileUtf8AndCleanup(out)).resolves.toBe("hi");
|
||||
});
|
||||
});
|
||||
@@ -247,6 +247,22 @@ describe("nodes camera helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects empty and malformed base64 payloads before writing", async () => {
|
||||
await withCameraTempDir(async (dir) => {
|
||||
const out = path.join(dir, "x.bin");
|
||||
for (const base64 of ["", " \n", "a", "a===", "not-base64!"]) {
|
||||
await expect(writeBase64ToFile(out, base64)).rejects.toThrow(/invalid base64/i);
|
||||
await expectPathMissing(out);
|
||||
}
|
||||
await expect(writeScreenRecordToFile(out, "not-base64!")).rejects.toThrow(/invalid base64/i);
|
||||
await expectPathMissing(out);
|
||||
await expect(writeScreenSnapshotToFile(out, "not-base64!")).rejects.toThrow(
|
||||
/invalid base64/i,
|
||||
);
|
||||
await expectPathMissing(out);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Camera payload validation and artifact writers for node media commands.
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64";
|
||||
import { toErrorObject } from "../infra/errors.js";
|
||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
import { normalizeHostname } from "../infra/net/hostname.js";
|
||||
@@ -202,12 +203,6 @@ export async function writeUrlToFile(
|
||||
return { path: filePath, bytes };
|
||||
}
|
||||
|
||||
function estimateDecodedBase64Bytes(base64: string): number {
|
||||
const normalized = base64.replace(/\s+/g, "");
|
||||
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
||||
return Math.floor((normalized.length * 3) / 4) - padding;
|
||||
}
|
||||
|
||||
/** Decode a base64 media payload to disk with preflight and post-decode size checks. */
|
||||
export async function writeBase64ToFile(
|
||||
filePath: string,
|
||||
@@ -215,10 +210,14 @@ export async function writeBase64ToFile(
|
||||
opts: { maxBytes?: number } = {},
|
||||
) {
|
||||
const maxBytes = opts.maxBytes ?? MAX_CAMERA_BASE64_BYTES;
|
||||
if (estimateDecodedBase64Bytes(base64) > maxBytes) {
|
||||
if (estimateBase64DecodedBytes(base64) > maxBytes) {
|
||||
throw new Error(`writeBase64ToFile: decoded payload exceeds max ${maxBytes}`);
|
||||
}
|
||||
const buf = Buffer.from(base64, "base64");
|
||||
const canonicalBase64 = canonicalizeBase64(base64);
|
||||
if (!canonicalBase64) {
|
||||
throw new Error("writeBase64ToFile: invalid base64 payload");
|
||||
}
|
||||
const buf = Buffer.from(canonicalBase64, "base64");
|
||||
if (buf.length > maxBytes) {
|
||||
throw new Error(`writeBase64ToFile: decoded ${buf.length} bytes, exceeds max ${maxBytes}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user