From 53bc2e15f846b24a9af5b2482f15513038009438 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 08:36:02 +0900 Subject: [PATCH 1/2] [#964] Add 1MB IPFS response size limit to prevent memory exhaustion Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- src/app/api/cron/backfill/route.ts | 7 ++++++- src/app/api/index/plot/route.ts | 4 ++++ src/app/api/index/storyline/route.ts | 4 ++++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ceefd287..59f987aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "0.1.51", + "version": "0.1.52", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "0.1.51", + "version": "0.1.52", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index ebd8a71e..4820407f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.51", + "version": "0.1.52", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/api/cron/backfill/route.ts b/src/app/api/cron/backfill/route.ts index 2658ff77..ea2ac380 100644 --- a/src/app/api/cron/backfill/route.ts +++ b/src/app/api/cron/backfill/route.ts @@ -13,6 +13,7 @@ import type { Database } from "../../../../../lib/supabase"; const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/"; const IPFS_TIMEOUT_MS = 10_000; +const IPFS_MAX_BYTES = 1_000_000; /** * How many blocks to scan per cron run (~5 min on Base = ~150 blocks at 2s/block). @@ -36,7 +37,11 @@ async function fetchIPFSContent(cid: string): Promise { signal: AbortSignal.timeout(IPFS_TIMEOUT_MS), }); if (!res.ok) return null; - return await res.text(); + const cl = res.headers.get("content-length"); + if (cl && parseInt(cl) > IPFS_MAX_BYTES) return null; + const text = await res.text(); + if (text.length > IPFS_MAX_BYTES) return null; + return text; } catch { return null; } diff --git a/src/app/api/index/plot/route.ts b/src/app/api/index/plot/route.ts index a5fca822..d47414e0 100644 --- a/src/app/api/index/plot/route.ts +++ b/src/app/api/index/plot/route.ts @@ -14,6 +14,7 @@ import type { Database } from "../../../../../lib/supabase"; const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/"; const IPFS_TIMEOUT_MS = 10_000; +const IPFS_MAX_BYTES = 1_000_000; /** PlotChained event topic0 (keccak256 of the event signature) */ const PLOT_CHAINED_TOPIC = encodeEventTopics({ @@ -93,7 +94,10 @@ export async function POST(req: Request) { signal: AbortSignal.timeout(IPFS_TIMEOUT_MS), }); if (!ipfsRes.ok) throw new Error(`IPFS status ${ipfsRes.status}`); + const cl = ipfsRes.headers.get("content-length"); + if (cl && parseInt(cl) > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); const ipfsContent = await ipfsRes.text(); + if (ipfsContent.length > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); // Verify IPFS content hash matches on-chain hash if (hashContent(ipfsContent) === contentHash) { content = ipfsContent; diff --git a/src/app/api/index/storyline/route.ts b/src/app/api/index/storyline/route.ts index 9cccbee2..6051f9db 100644 --- a/src/app/api/index/storyline/route.ts +++ b/src/app/api/index/storyline/route.ts @@ -17,6 +17,7 @@ import { awardWritePoints } from "../../../../../lib/airdrop/award"; const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/"; const IPFS_TIMEOUT_MS = 10_000; +const IPFS_MAX_BYTES = 1_000_000; /** StorylineCreated event topic0 */ const STORYLINE_CREATED_TOPIC = encodeEventTopics({ @@ -121,7 +122,10 @@ export async function POST(req: Request) { signal: AbortSignal.timeout(IPFS_TIMEOUT_MS), }); if (!ipfsRes.ok) throw new Error(`IPFS status ${ipfsRes.status}`); + const cl = ipfsRes.headers.get("content-length"); + if (cl && parseInt(cl) > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); const ipfsContent = await ipfsRes.text(); + if (ipfsContent.length > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); // Verify IPFS content hash matches on-chain hash if (hashContent(ipfsContent) === openingHash) { genesisContent = ipfsContent; From 65a0b54cfab081183e5fa767dce5a40859ff8cb9 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 08:37:47 +0900 Subject: [PATCH 2/2] [#964] Measure IPFS body size in bytes via TextEncoder instead of string length Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/cron/backfill/route.ts | 2 +- src/app/api/index/plot/route.ts | 2 +- src/app/api/index/storyline/route.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/cron/backfill/route.ts b/src/app/api/cron/backfill/route.ts index ea2ac380..7cacbfc6 100644 --- a/src/app/api/cron/backfill/route.ts +++ b/src/app/api/cron/backfill/route.ts @@ -40,7 +40,7 @@ async function fetchIPFSContent(cid: string): Promise { const cl = res.headers.get("content-length"); if (cl && parseInt(cl) > IPFS_MAX_BYTES) return null; const text = await res.text(); - if (text.length > IPFS_MAX_BYTES) return null; + if (new TextEncoder().encode(text).byteLength > IPFS_MAX_BYTES) return null; return text; } catch { return null; diff --git a/src/app/api/index/plot/route.ts b/src/app/api/index/plot/route.ts index d47414e0..5e5415cd 100644 --- a/src/app/api/index/plot/route.ts +++ b/src/app/api/index/plot/route.ts @@ -97,7 +97,7 @@ export async function POST(req: Request) { const cl = ipfsRes.headers.get("content-length"); if (cl && parseInt(cl) > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); const ipfsContent = await ipfsRes.text(); - if (ipfsContent.length > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); + if (new TextEncoder().encode(ipfsContent).byteLength > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); // Verify IPFS content hash matches on-chain hash if (hashContent(ipfsContent) === contentHash) { content = ipfsContent; diff --git a/src/app/api/index/storyline/route.ts b/src/app/api/index/storyline/route.ts index 6051f9db..bd336bf4 100644 --- a/src/app/api/index/storyline/route.ts +++ b/src/app/api/index/storyline/route.ts @@ -125,7 +125,7 @@ export async function POST(req: Request) { const cl = ipfsRes.headers.get("content-length"); if (cl && parseInt(cl) > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); const ipfsContent = await ipfsRes.text(); - if (ipfsContent.length > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); + if (new TextEncoder().encode(ipfsContent).byteLength > IPFS_MAX_BYTES) throw new Error("IPFS content too large"); // Verify IPFS content hash matches on-chain hash if (hashContent(ipfsContent) === openingHash) { genesisContent = ipfsContent;