mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:08:25 +00:00
fix: execute sandboxed file ops inside containers (#4026)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 795ec6aa2f
Co-authored-by: davidbors-snyk <240482518+davidbors-snyk@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
@@ -29,6 +29,22 @@ function buildDeterministicBytes(length: number): Buffer {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function createLargeTestJpeg(): Promise<{ buffer: Buffer; file: string }> {
|
||||
const buffer = await sharp({
|
||||
create: {
|
||||
width: 1600,
|
||||
height: 1600,
|
||||
channels: 3,
|
||||
background: "#ff0000",
|
||||
},
|
||||
})
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer();
|
||||
|
||||
const file = await writeTempFile(buffer, ".jpg");
|
||||
return { buffer, file };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tmpFiles.map((file) => fs.rm(file, { force: true })));
|
||||
tmpFiles.length = 0;
|
||||
@@ -70,6 +86,25 @@ describe("web media loading", () => {
|
||||
expect(result.buffer.length).toBeLessThan(buffer.length);
|
||||
});
|
||||
|
||||
it("optimizes images when options object omits optimizeImages", async () => {
|
||||
const { buffer, file } = await createLargeTestJpeg();
|
||||
const cap = Math.max(1, Math.floor(buffer.length * 0.8));
|
||||
|
||||
const result = await loadWebMedia(file, { maxBytes: cap });
|
||||
|
||||
expect(result.buffer.length).toBeLessThanOrEqual(cap);
|
||||
expect(result.buffer.length).toBeLessThan(buffer.length);
|
||||
});
|
||||
|
||||
it("allows callers to disable optimization via options object", async () => {
|
||||
const { buffer, file } = await createLargeTestJpeg();
|
||||
const cap = Math.max(1, Math.floor(buffer.length * 0.8));
|
||||
|
||||
await expect(loadWebMedia(file, { maxBytes: cap, optimizeImages: false })).rejects.toThrow(
|
||||
/Media exceeds/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("sniffs mime before extension when loading local files", async () => {
|
||||
const pngBuffer = await sharp({
|
||||
create: { width: 2, height: 2, channels: 3, background: "#00ff00" },
|
||||
|
||||
@@ -28,6 +28,7 @@ type WebMediaOptions = {
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
/** Allowed root directories for local path reads. "any" skips the check (caller already validated). */
|
||||
localRoots?: string[] | "any";
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
|
||||
function getDefaultLocalRoots(): string[] {
|
||||
@@ -165,7 +166,13 @@ async function loadWebMediaInternal(
|
||||
mediaUrl: string,
|
||||
options: WebMediaOptions = {},
|
||||
): Promise<WebMediaResult> {
|
||||
const { maxBytes, optimizeImages = true, ssrfPolicy, localRoots } = options;
|
||||
const {
|
||||
maxBytes,
|
||||
optimizeImages = true,
|
||||
ssrfPolicy,
|
||||
localRoots,
|
||||
readFile: readFileOverride,
|
||||
} = options;
|
||||
// Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.)
|
||||
if (mediaUrl.startsWith("file://")) {
|
||||
try {
|
||||
@@ -267,7 +274,7 @@ async function loadWebMediaInternal(
|
||||
await assertLocalMediaAllowed(mediaUrl, localRoots);
|
||||
|
||||
// Local path
|
||||
const data = await fs.readFile(mediaUrl);
|
||||
const data = readFileOverride ? await readFileOverride(mediaUrl) : await fs.readFile(mediaUrl);
|
||||
const mime = await detectMime({ buffer: data, filePath: mediaUrl });
|
||||
const kind = mediaKindFromMime(mime);
|
||||
let fileName = path.basename(mediaUrl) || undefined;
|
||||
@@ -287,27 +294,39 @@ async function loadWebMediaInternal(
|
||||
|
||||
export async function loadWebMedia(
|
||||
mediaUrl: string,
|
||||
maxBytes?: number,
|
||||
maxBytesOrOptions?: number | WebMediaOptions,
|
||||
options?: { ssrfPolicy?: SsrFPolicy; localRoots?: string[] | "any" },
|
||||
): Promise<WebMediaResult> {
|
||||
if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === undefined) {
|
||||
return await loadWebMediaInternal(mediaUrl, {
|
||||
maxBytes: maxBytesOrOptions,
|
||||
optimizeImages: true,
|
||||
ssrfPolicy: options?.ssrfPolicy,
|
||||
localRoots: options?.localRoots,
|
||||
});
|
||||
}
|
||||
return await loadWebMediaInternal(mediaUrl, {
|
||||
maxBytes,
|
||||
optimizeImages: true,
|
||||
ssrfPolicy: options?.ssrfPolicy,
|
||||
localRoots: options?.localRoots,
|
||||
...maxBytesOrOptions,
|
||||
optimizeImages: maxBytesOrOptions.optimizeImages ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadWebMediaRaw(
|
||||
mediaUrl: string,
|
||||
maxBytes?: number,
|
||||
maxBytesOrOptions?: number | WebMediaOptions,
|
||||
options?: { ssrfPolicy?: SsrFPolicy; localRoots?: string[] | "any" },
|
||||
): Promise<WebMediaResult> {
|
||||
if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === undefined) {
|
||||
return await loadWebMediaInternal(mediaUrl, {
|
||||
maxBytes: maxBytesOrOptions,
|
||||
optimizeImages: false,
|
||||
ssrfPolicy: options?.ssrfPolicy,
|
||||
localRoots: options?.localRoots,
|
||||
});
|
||||
}
|
||||
return await loadWebMediaInternal(mediaUrl, {
|
||||
maxBytes,
|
||||
...maxBytesOrOptions,
|
||||
optimizeImages: false,
|
||||
ssrfPolicy: options?.ssrfPolicy,
|
||||
localRoots: options?.localRoots,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user