mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(browser): reject malformed proxy file base64 (#115045)
* fix(browser): reject malformed proxy file base64 * fix(browser): preserve empty proxy downloads --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -38,6 +38,30 @@ describe("persistBrowserProxyFiles", () => {
|
||||
await expect(fs.readFile(savedPath ?? "", "utf8")).resolves.toBe("hello from browser proxy");
|
||||
});
|
||||
|
||||
it("persists legitimate empty browser proxy downloads", async () => {
|
||||
const sourcePath = "/tmp/empty-browser-download.bin";
|
||||
const mapping = await persistBrowserProxyFiles([
|
||||
{ path: sourcePath, base64: "", mimeType: "application/octet-stream" },
|
||||
]);
|
||||
|
||||
const savedPath = mapping.get(sourcePath);
|
||||
expect(typeof savedPath).toBe("string");
|
||||
await expect(fs.stat(savedPath ?? "")).resolves.toMatchObject({ size: 0 });
|
||||
await expect(fs.readFile(savedPath ?? "")).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "valid unpadded base64", base64: "aGVsbG8" },
|
||||
{ name: "valid whitespace-separated base64", base64: " aG Vs bG8= \n" },
|
||||
])("persists $name without corrupting the download", async ({ base64 }) => {
|
||||
const sourcePath = "/tmp/normalized-browser-download.txt";
|
||||
const mapping = await persistBrowserProxyFiles([
|
||||
{ path: sourcePath, base64, mimeType: "text/plain" },
|
||||
]);
|
||||
|
||||
await expect(fs.readFile(mapping.get(sourcePath) ?? "", "utf8")).resolves.toBe("hello");
|
||||
});
|
||||
|
||||
it("persists a file at the proxy limit above the shared media default", async () => {
|
||||
const sourcePath = "/tmp/above-default.bin";
|
||||
const buffer = Buffer.alloc(BROWSER_PROXY_MAX_FILE_BYTES, 0x41);
|
||||
@@ -104,6 +128,64 @@ describe("persistBrowserProxyFiles", () => {
|
||||
).rejects.toHaveProperty("code", "ENOENT");
|
||||
});
|
||||
|
||||
it("rejects malformed base64 before persisting files", async () => {
|
||||
const error = await persistBrowserProxyFiles([
|
||||
{
|
||||
path: "/tmp/malformed.bin",
|
||||
base64: "aGVsbG8$",
|
||||
mimeType: "application/octet-stream",
|
||||
},
|
||||
]).then(
|
||||
() => null,
|
||||
(err: unknown) => err,
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe("browser proxy file contains malformed base64 data");
|
||||
|
||||
await expect(
|
||||
fs.stat(path.join(tempHome.home, ".openclaw", "media", "browser")),
|
||||
).rejects.toHaveProperty("code", "ENOENT");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "invalid alphabet", base64: "aGVsbG8$" },
|
||||
{ name: "invalid padding", base64: "aGVsbG8===" },
|
||||
{ name: "nonzero padding bits", base64: "ZE==" },
|
||||
{ name: "impossible unpadded length", base64: "S" },
|
||||
{ name: "whitespace without encoded data", base64: " \n\t" },
|
||||
])("rejects $name before creating the media directory", async ({ base64 }) => {
|
||||
await expect(
|
||||
persistBrowserProxyFiles([
|
||||
{
|
||||
path: "/tmp/malformed-browser-download.bin",
|
||||
base64,
|
||||
mimeType: "application/octet-stream",
|
||||
},
|
||||
]),
|
||||
).rejects.toThrow("browser proxy file contains malformed base64 data");
|
||||
|
||||
await expect(
|
||||
fs.stat(path.join(tempHome.home, ".openclaw", "media", "browser")),
|
||||
).rejects.toHaveProperty("code", "ENOENT");
|
||||
});
|
||||
|
||||
it("rejects a later malformed file without persisting an earlier valid file", async () => {
|
||||
await expect(
|
||||
persistBrowserProxyFiles([
|
||||
{
|
||||
path: "/tmp/valid-browser-download.txt",
|
||||
base64: Buffer.from("valid browser download").toString("base64"),
|
||||
mimeType: "text/plain",
|
||||
},
|
||||
{ path: "/tmp/malformed-browser-download.bin", base64: "ZE==" },
|
||||
]),
|
||||
).rejects.toThrow("browser proxy file contains malformed base64 data");
|
||||
|
||||
await expect(
|
||||
fs.stat(path.join(tempHome.home, ".openclaw", "media", "browser")),
|
||||
).rejects.toHaveProperty("code", "ENOENT");
|
||||
});
|
||||
|
||||
it("rejects too many files before persisting any", async () => {
|
||||
const files = Array.from({ length: BROWSER_PROXY_MAX_FILES + 1 }, (_, index) => ({
|
||||
path: `/tmp/file-${index}.bin`,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Persists files returned by node-hosted browser proxy calls and rewrites
|
||||
* proxied result paths to local saved media paths.
|
||||
*/
|
||||
import { canonicalizeBase64, estimateBase64DecodedBytes } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
assertBrowserProxyFileCountWithinLimit,
|
||||
assertBrowserProxyFileBytesWithinLimits,
|
||||
@@ -13,6 +14,19 @@ import {
|
||||
} from "../browser-proxy-envelope.js";
|
||||
import { saveMediaBuffer } from "../media/store.js";
|
||||
|
||||
function decodeBrowserProxyFileBase64(file: BrowserProxyFile, totalBytes: number): Buffer {
|
||||
const estimatedBytes = estimateBase64DecodedBytes(file.base64);
|
||||
assertBrowserProxyFileBytesWithinLimits(estimatedBytes, totalBytes + estimatedBytes);
|
||||
// The shared validator rejects empty input, but zero-byte downloads are valid files.
|
||||
const canonicalBase64 = file.base64 === "" ? "" : canonicalizeBase64(file.base64);
|
||||
if (canonicalBase64 === undefined) {
|
||||
throw new Error("browser proxy file contains malformed base64 data");
|
||||
}
|
||||
const buffer = Buffer.from(canonicalBase64, "base64");
|
||||
assertBrowserProxyFileBytesWithinLimits(buffer.byteLength, totalBytes + buffer.byteLength);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/** Persist proxy-returned files and return a remote-path to local-path map. */
|
||||
export async function persistBrowserProxyFiles(files: BrowserProxyFile[] | undefined) {
|
||||
if (!files || files.length === 0) {
|
||||
@@ -22,9 +36,8 @@ export async function persistBrowserProxyFiles(files: BrowserProxyFile[] | undef
|
||||
const decoded: Array<{ file: BrowserProxyFile; buffer: Buffer }> = [];
|
||||
let totalBytes = 0;
|
||||
for (const file of files) {
|
||||
const buffer = Buffer.from(file.base64, "base64");
|
||||
const buffer = decodeBrowserProxyFileBase64(file, totalBytes);
|
||||
totalBytes += buffer.byteLength;
|
||||
assertBrowserProxyFileBytesWithinLimits(buffer.byteLength, totalBytes);
|
||||
decoded.push({ file, buffer });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user