From d04af5cd758ac213c5fb913fc96ccb8ce460441e Mon Sep 17 00:00:00 2001 From: Lawyered Date: Tue, 17 Feb 2026 11:33:08 -0500 Subject: [PATCH] web: add integrity check for marked CDN and cap highlight regex input (#109) * web: add integrity check for marked CDN and cap highlight regex input * web: normalize memory search query before snippet+highlight matching * web: place memory query length constant with top-level config --------- Co-authored-by: Clawyered Co-authored-by: Illia Polosukhin --- src/channels/web/static/app.js | 21 +++++++++++++++------ src/channels/web/static/index.html | 6 +++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index cdc1e06d..398f2f54 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -12,6 +12,7 @@ let loadingOlder = false; let jobEvents = new Map(); // job_id -> Array of events let jobListRefreshTimer = null; const JOB_EVENTS_CAP = 500; +const MEMORY_SEARCH_QUERY_MAX_LENGTH = 100; // --- Auth --- @@ -1001,9 +1002,12 @@ function buildBreadcrumb(path) { } function searchMemory(query) { + const normalizedQuery = normalizeSearchQuery(query); + if (!normalizedQuery) return; + apiFetch('/api/memory/search', { method: 'POST', - body: { query, limit: 20 }, + body: { query: normalizedQuery, limit: 20 }, }).then((data) => { const tree = document.getElementById('memory-tree'); tree.innerHTML = ''; @@ -1014,18 +1018,23 @@ function searchMemory(query) { for (const result of data.results) { const item = document.createElement('div'); item.className = 'search-result'; - const snippet = snippetAround(result.content, query, 120); + const snippet = snippetAround(result.content, normalizedQuery, 120); item.innerHTML = '
' + escapeHtml(result.path) + '
' - + '
' + highlightQuery(snippet, query) + '
'; + + '
' + highlightQuery(snippet, normalizedQuery) + '
'; item.addEventListener('click', () => readMemoryFile(result.path)); tree.appendChild(item); } }).catch(() => {}); } +function normalizeSearchQuery(query) { + return (typeof query === 'string' ? query : '').slice(0, MEMORY_SEARCH_QUERY_MAX_LENGTH); +} + function snippetAround(text, query, len) { + const normalizedQuery = normalizeSearchQuery(query); const lower = text.toLowerCase(); - const idx = lower.indexOf(query.toLowerCase()); + const idx = lower.indexOf(normalizedQuery.toLowerCase()); if (idx < 0) return text.substring(0, len); const start = Math.max(0, idx - Math.floor(len / 2)); const end = Math.min(text.length, start + len); @@ -1038,11 +1047,11 @@ function snippetAround(text, query, len) { function highlightQuery(text, query) { if (!query) return escapeHtml(text); const escaped = escapeHtml(text); - const queryEscaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const normalizedQuery = normalizeSearchQuery(query); + const queryEscaped = normalizedQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const re = new RegExp('(' + queryEscaped + ')', 'gi'); return escaped.replace(re, '$1'); } - // --- Logs --- const LOG_MAX_ENTRIES = 2000; diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html index bf6c227c..ddcf6892 100644 --- a/src/channels/web/static/index.html +++ b/src/channels/web/static/index.html @@ -5,7 +5,11 @@ IronClaw - +