Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion typescript/agentkit/src/action-providers/zora/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ export const CreateCoinSchema = z
name: z.string().describe("The name of the coin to create"),
symbol: z.string().describe("The symbol of the coin to create"),
description: z.string().describe("The description of the coin"),
image: z.string().describe("Local image file path or URI (ipfs:// or https://)"),
image: z
.string()
.describe(
"HTTPS or ipfs:// URI of the coin image, or a local image path under the working directory",
),
category: z
.string()
.nullable()
Expand Down
41 changes: 41 additions & 0 deletions typescript/agentkit/src/action-providers/zora/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

import { generateZoraTokenUri, resolveSafeLocalImagePath } from "./utils";

describe("zora image path / URI guards", () => {
describe("resolveSafeLocalImagePath", () => {
it("allows files under cwd and rejects escapes", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "zora-img-"));
const prev = process.cwd();
try {
process.chdir(tmp);
const inside = path.join(tmp, "token.png");
fs.writeFileSync(inside, "x");
expect(resolveSafeLocalImagePath("token.png")).toBe(fs.realpathSync(inside));
expect(() => resolveSafeLocalImagePath("../outside.png")).toThrow(/working directory/i);
expect(() => resolveSafeLocalImagePath("/etc/passwd")).toThrow(/working directory/i);
} finally {
process.chdir(prev);
fs.rmSync(tmp, { recursive: true, force: true });
}
});
});

describe("generateZoraTokenUri", () => {
const pinataConfig = { jwt: "test-jwt" };

it("rejects non-https remote schemes before reading as a local file", async () => {
await expect(
generateZoraTokenUri({
name: "t",
symbol: "T",
description: "d",
image: "http://127.0.0.1/secret.png",
pinataConfig,
}),
).rejects.toThrow(/https:\/\/ or ipfs:\/\//i);
});
});
});
28 changes: 25 additions & 3 deletions typescript/agentkit/src/action-providers/zora/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ interface TokenUriParams {
pinataConfig: PinataConfig;
}

/**
* Resolve a local image path and require it to stay under process.cwd()
* (realpath), so agent-supplied paths cannot read arbitrary files for Pinata.
* Twin of flaunch resolveSafeLocalImagePath (#1424).
*/
export function resolveSafeLocalImagePath(imageFileName: string): string {
const root = fs.realpathSync(process.cwd());
const resolved = path.resolve(root, imageFileName);
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
throw new Error("Local image path must be within the working directory");
}
const real = fs.realpathSync(resolved);
if (real !== root && !real.startsWith(root + path.sep)) {
throw new Error("Local image path escapes the working directory");
}
return real;
}

/**
* Reads a local file and converts it to base64
*
Expand All @@ -56,15 +74,16 @@ interface TokenUriParams {
async function readFileAsBase64(
imageFileName: string,
): Promise<{ base64: string; mimeType: string }> {
const safePath = resolveSafeLocalImagePath(imageFileName);
return new Promise((resolve, reject) => {
fs.readFile(imageFileName, (err, data) => {
fs.readFile(safePath, (err, data) => {
if (err) {
reject(new Error(`Failed to read file: ${err.message}`));
return;
}

// Determine mime type based on file extension
const extension = path.extname(imageFileName).toLowerCase();
const extension = path.extname(safePath).toLowerCase();
let mimeType = "application/octet-stream"; // default

if (extension === ".png") mimeType = "image/png";
Expand Down Expand Up @@ -222,8 +241,11 @@ export async function generateZoraTokenUri(params: TokenUriParams): Promise<{
// Check if image is already a URI (ipfs:// or https://)
if (params.image.startsWith("ipfs://") || params.image.startsWith("https://")) {
imageUri = params.image;
} else if (params.image.includes("://")) {
// http:// and other schemes must not fall through to fs.readFile
throw new Error("Remote Zora images must use https:// or ipfs://");
} else {
// Handle local file
// Local file: path must resolve under process.cwd()
const { base64, mimeType } = await readFileAsBase64(params.image);
const fileName = path.basename(params.image);

Expand Down
Loading