From ef7fc05dcc4b57f741d00c0f644298cb93d2ee13 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Tue, 11 Aug 2026 18:27:42 -0700 Subject: [PATCH] fix: reject feedback origins longer than the source_origin column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readTargetParam accepted values up to 3000 chars but the raw origin is stored verbatim into source_origin VARCHAR(2048) (MySQL/Postgres), so a 2049-3000 char origin passed every validation and then blew up the INSERT with an HTTP 500 on Postgres/strict MySQL — or was silently truncated on non-strict MySQL, corrupting the abuse-forensics value the column exists for. Cap the param at the column size. --- .../feedback/AppFeedbackController.test.ts | 14 ++++++++++++++ .../controllers/feedback/AppFeedbackController.ts | 10 +++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/backend/controllers/feedback/AppFeedbackController.test.ts b/src/backend/controllers/feedback/AppFeedbackController.test.ts index 743c7ec15..b92edb3c2 100644 --- a/src/backend/controllers/feedback/AppFeedbackController.test.ts +++ b/src/backend/controllers/feedback/AppFeedbackController.test.ts @@ -357,6 +357,20 @@ describe('AppFeedbackController POST /', () => { ).rejects.toMatchObject({ statusCode: 400 }); }); + it('throws 400 when the origin exceeds the stored column size', async () => { + // source_origin is VARCHAR(2048) on MySQL/Postgres; a longer origin + // must be rejected up front, not fail (or silently truncate) at the + // INSERT after passing every other validation. + mockEmailConfigured(); + const { userId: ownerId } = await makeUser(); + const app = await makeApp(ownerId, { feedbackEnabled: true }); + const origin = `${new URL(app.index_url).origin}/${'x'.repeat(2500)}`; + const { actor } = await makeUser(); + await expect( + submit(actor, { origin, message: 'hi' }), + ).rejects.toMatchObject({ statusCode: 400 }); + }); + it('throws 403 feedback_not_enabled when the app has not opted in', async () => { mockEmailConfigured(); const { userId: ownerId } = await makeUser(); diff --git a/src/backend/controllers/feedback/AppFeedbackController.ts b/src/backend/controllers/feedback/AppFeedbackController.ts index 0fa4cc2f5..2e972ef68 100644 --- a/src/backend/controllers/feedback/AppFeedbackController.ts +++ b/src/backend/controllers/feedback/AppFeedbackController.ts @@ -38,8 +38,16 @@ import { PuterController } from '../types.js'; /** Sanity cap on the raw body field; the service enforces the real limit. */ const RAW_MESSAGE_CAP = 50_000; +// Upper bound on the `app`/`origin` target params. Must not exceed the +// `source_origin` column (VARCHAR(2048) on MySQL/Postgres): the raw origin is +// stored verbatim, and a longer value would fail the INSERT after passing +// every validation — or be silently truncated on non-strict MySQL. +const TARGET_PARAM_MAX_LENGTH = 2048; + const readTargetParam = (value: unknown): string | undefined => { - return typeof value === 'string' && value.length > 0 && value.length <= 3000 + return typeof value === 'string' && + value.length > 0 && + value.length <= TARGET_PARAM_MAX_LENGTH ? value : undefined; };