diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index a0777848..532f9c21 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -212,27 +212,77 @@ 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 {}; + + // 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); + } + let bytes: Uint8Array; + if (chunks.length === 1) { + bytes = chunks[0]; + } else { + bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + } + const text = new TextDecoder().decode(bytes); + 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; } /**