From 14fdad300178a4e8b3bb50182ee41fbf49aa0074 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Fri, 21 Aug 2026 00:20:00 -0300 Subject: [PATCH] fix(threads): sanitize object keys, not just values, before storing a part payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serializePayload's JSON.stringify replacer strips NUL bytes and lone surrogates from string VALUES so a part payload never trips Postgres's SQLSTATE 22P05 on INSERT. But a replacer function can only substitute a key's mapped value, never the key text itself — an object key carrying a NUL byte or an unpaired surrogate (e.g. a tool result keyed by attacker/content-derived strings) still serializes to a raw \u0000 escape and hits the exact same 22P05 this code exists to prevent, stranding the run in_progress forever. Adds sanitizeKeysForPg to rebuild the payload tree with clean keys before stringifying. Normal keys are byte-identical (both regexes are no-ops on clean input), so ids derived from the payload stay stable. --- .../src/storage/thread-message-parts.test.ts | Bin 2470 -> 3058 bytes apps/api/src/storage/thread-message-parts.ts | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/api/src/storage/thread-message-parts.test.ts b/apps/api/src/storage/thread-message-parts.test.ts index 7448e8052efc5aa38c74df4aaa6a4f3702f04eb4..0005cf7a2454eba55651512d35d59ba27496583f 100644 GIT binary patch delta 305 zcmZ1`{7HPnBF=g)1qFS51^=Y1)Z`L{?9|F)g}l_%6orzERE6Ti+*Ad>P#^u`(xRgL z^u!XNP)Si{K|y9-I#@wjVoqslu|j%ki9%vhep#v>(6G!B4JELuVvtsaq{0 ecxpx~fu!H>-wTveiUrx^l&S%-TM2G7$anzD#AZ = {}; + for (const [key, v] of Object.entries(value)) { + out[sanitizeForPg(key)] = sanitizeKeysForPg(v); + } + return out; + } + return value; +} + export function serializePayload(payload: unknown): string { if ( estimatePayloadBytes(payload, MAX_PART_PAYLOAD_BYTES) > @@ -79,7 +92,7 @@ export function serializePayload(payload: unknown): string { // A JSON replacer visits every string in the payload tree; clean strings pass // through untouched (byte-identical output, so ids derived from the payload // stay stable). - return JSON.stringify(payload, (_key, value) => + return JSON.stringify(sanitizeKeysForPg(payload), (_key, value) => typeof value === "string" ? sanitizeForPg(value) : value, ); }