From 844ebc5e828db9f085a40019b9946b12e6acf239 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 30 Mar 2026 19:55:16 +0100 Subject: [PATCH 1/3] [#639] Harden resolveAgentURI with timeout, size limits, validation - Add 5s AbortController timeout for remote fetches (https/ipfs) - Check res.ok before parsing response body - Enforce 50KB size limit on all payloads (remote, data: URI, raw JSON) - Wrap entire function in try/catch for graceful JSON parse error handling - Return empty object instead of throwing on any failure Fixes realproject7/plotlink#639 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/erc8004.ts | 51 ++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index a0777848..39c5dced 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -212,27 +212,48 @@ export async function detectWriterType( } } +const MAX_URI_BYTES = 50 * 1024; // 50 KB size limit +const FETCH_TIMEOUT_MS = 5000; // 5-second timeout + /** * Resolve an agent URI string to a parsed JSON object. * Handles raw JSON, data: URIs (base64 + URL-encoded), https://, and ipfs://. + * Includes timeout, size limits, and response validation for safety. */ export async function resolveAgentURI(uri: string): Promise> { - if (uri.startsWith("{")) { - return JSON.parse(uri); - } - if (uri.startsWith("data:")) { - const comma = uri.indexOf(","); - const payload = comma >= 0 ? uri.slice(comma + 1) : uri; - return JSON.parse( - uri.includes("base64") ? atob(payload) : decodeURIComponent(payload), - ); + try { + if (uri.startsWith("{")) { + if (uri.length > MAX_URI_BYTES) return {}; + return JSON.parse(uri); + } + if (uri.startsWith("data:")) { + const comma = uri.indexOf(","); + const payload = comma >= 0 ? uri.slice(comma + 1) : uri; + if (payload.length > MAX_URI_BYTES) return {}; + return JSON.parse( + uri.includes("base64") ? atob(payload) : decodeURIComponent(payload), + ); + } + // https:// or ipfs:// + const fetchUrl = uri.startsWith("ipfs://") + ? uri.replace("ipfs://", "https://ipfs.io/ipfs/") + : uri; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const res = await fetch(fetchUrl, { signal: controller.signal }); + if (!res.ok) return {}; + + const text = await res.text(); + if (text.length > MAX_URI_BYTES) return {}; + return JSON.parse(text); + } finally { + clearTimeout(timeout); + } + } catch { + return {}; } - // https:// or ipfs:// - const fetchUrl = uri.startsWith("ipfs://") - ? uri.replace("ipfs://", "https://ipfs.io/ipfs/") - : uri; - const res = await fetch(fetchUrl); - return (await res.json()) as Record; } /** From bc984e6f365a8e29d226decdf1f7fbde4b6df210 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 30 Mar 2026 19:57:10 +0100 Subject: [PATCH 2/3] [#639] Stream response body with size cap instead of buffering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check Content-Length header for early rejection, then stream body via ReadableStream reader and abort once accumulated bytes exceed 50KB — avoids buffering oversized responses into memory. Addresses T2a review feedback on PR #649. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/erc8004.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index 39c5dced..0f18a75b 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -245,8 +245,28 @@ export async function resolveAgentURI(uri: string): Promise MAX_URI_BYTES) return {}; + // Reject early if Content-Length exceeds limit + const contentLength = res.headers.get("content-length"); + if (contentLength && parseInt(contentLength, 10) > MAX_URI_BYTES) return {}; + + // Stream body with size cap to avoid buffering oversized responses + const reader = res.body?.getReader(); + if (!reader) return {}; + const chunks: Uint8Array[] = []; + let totalBytes = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (totalBytes > MAX_URI_BYTES) { + reader.cancel(); + return {}; + } + chunks.push(value); + } + const text = new TextDecoder().decode( + chunks.length === 1 ? chunks[0] : Buffer.concat(chunks), + ); return JSON.parse(text); } finally { clearTimeout(timeout); From 66000804e2b6d83e156ab8fff503fffda3a3c744 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 30 Mar 2026 19:59:00 +0100 Subject: [PATCH 3/3] [#639] Replace Buffer.concat with browser-safe Uint8Array accumulation Buffer.concat is Node-only and this module is imported by client components. Use plain Uint8Array concatenation instead. Addresses T2a review feedback on PR #649. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/erc8004.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index 0f18a75b..532f9c21 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -264,9 +264,18 @@ export async function resolveAgentURI(uri: string): Promise