From cc6b525f57ac53c91e4da5e8daff02fe5cf37fda Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 16 May 2026 21:41:42 +0700 Subject: [PATCH] fix(frontend): cap DataTable filter input at 200 chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A paste of multi-KB content into the filter would land in the URL verbatim (`?q=` plus the raw blob), which exceeds the practical reverse-proxy limit (~2–4 KB) and breaks the share-link experience. The user-facing entry point is a single ``, so a DOM-level `maxLength` is sufficient — it truncates both typing and paste before the value ever reaches React state, the URL, or localStorage. 200 chars is well above any realistic search term while leaving plenty of headroom for non-ASCII expansion under percent-encoding. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/ui/data-table.test.tsx | 21 +++++++++++++++++++ frontend/src/components/ui/data-table.tsx | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/frontend/src/components/ui/data-table.test.tsx b/frontend/src/components/ui/data-table.test.tsx index df46d15b..d5c43a1e 100644 --- a/frontend/src/components/ui/data-table.test.tsx +++ b/frontend/src/components/ui/data-table.test.tsx @@ -319,6 +319,27 @@ describe('DataTable — empty results', () => { const secondInput = screen.getByRole('textbox'); expect(secondInput.getAttribute('id')).not.toBe(firstId); }); + + it('caps the filter input length so a paste of multi-KB content cannot blow past URL limits', () => { + render( + + columns={COLUMNS} + data={ROWS} + filterColumn="name" + filterValue="" + onFilterChange={() => { + /* no-op */ + }} + />, + { wrapper: Wrapper }, + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + // The DOM `maxLength` is the only choke point we need — `` + // truncates both typing and paste at this boundary, which keeps + // shared `?q=` URLs under the practical reverse-proxy limit (~2–4 KB). + expect(input.maxLength).toBe(200); + }); }); interface MultiRow { diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx index c4c77a0f..352ad3ac 100644 --- a/frontend/src/components/ui/data-table.tsx +++ b/frontend/src/components/ui/data-table.tsx @@ -155,6 +155,13 @@ interface DataTableFilterProps { } const FILTER_DEBOUNCE_MS = 150; +// Hard cap on the filter query length. 200 chars is more than any realistic +// search term and protects against pathological inputs (paste of a multi-KB +// chunk) that would otherwise blow past URL limits — browsers handle ~8 KB, +// reverse-proxies typically cap at 2–4 KB, so a 5 KB share-link becomes +// unreliable. `` truncates typing and paste at the DOM +// boundary, which is the only entry point users have here. +const FILTER_MAX_LENGTH = 200; /** * Search input for the table's global filter. Keystrokes update an internal @@ -211,6 +218,7 @@ const DataTableFilter = ({ onQueryChange, placeholder, query }: DataTableFilterP aria-label={placeholder} autoComplete="off" id={fieldId} + maxLength={FILTER_MAX_LENGTH} name={fieldId} onChange={(event) => setLocalValue(event.target.value)} placeholder={placeholder}