diff --git a/server/public/admin-account-detail.html b/server/public/admin-account-detail.html index 0fc0124666..890cbaa084 100644 --- a/server/public/admin-account-detail.html +++ b/server/public/admin-account-detail.html @@ -1688,12 +1688,12 @@

Delete Workspace

if (a.enrichment.employee_count_range || a.enrichment.employee_count) profileHtml += signalRow('Employees', a.enrichment.employee_count_range || a.enrichment.employee_count); if (a.enrichment.founded_year) profileHtml += signalRow('Founded', a.enrichment.founded_year); if (a.enrichment.city || a.enrichment.country) profileHtml += signalRow('Location', [a.enrichment.city, a.enrichment.country].filter(Boolean).join(', ')); - if (a.enrichment.linkedin_url) profileHtml += signalRow('LinkedIn', `View Profile`); + if (a.enrichment.linkedin_url) profileHtml += linkedInSignalRow(a.enrichment.linkedin_url); if (a.enrichment.description) { profileHtml += `
${escapeHtml(a.enrichment.description)}
`; } if (a.enrichment.enriched_at) { - profileHtml += `
Updated ${formatDate(a.enrichment.enriched_at)} via ${a.enrichment.source || 'enrichment'}
`; + profileHtml += enrichmentAttribution(a.enrichment.enriched_at, a.enrichment.source); } document.getElementById('companyProfileContent').innerHTML = profileHtml; @@ -2824,7 +2824,29 @@

${label}${value}`; + return `
${escapeHtml(label)}${escapeHtml(value)}
`; + } + + function getSafeLinkedInUrl(value) { + if (typeof value !== 'string' || value.length > 2048) return null; + try { + const parsed = new URL(value); + const isLinkedInHost = parsed.hostname === 'linkedin.com' || parsed.hostname.endsWith('.linkedin.com'); + if (parsed.protocol !== 'https:' || parsed.username || parsed.password || !isLinkedInHost) return null; + return parsed.href; + } catch { + return null; + } + } + + function linkedInSignalRow(value) { + const safeUrl = getSafeLinkedInUrl(value); + if (!safeUrl) return signalRow('LinkedIn', value); + return `
LinkedInView profile
`; + } + + function enrichmentAttribution(enrichedAt, source) { + return `
Updated ${escapeHtml(formatDate(enrichedAt))} via ${escapeHtml(source || 'enrichment')}
`; } function formatCompanyType(type) { diff --git a/server/public/admin-addie.html b/server/public/admin-addie.html index a766fec079..96ca579aad 100644 --- a/server/public/admin-addie.html +++ b/server/public/admin-addie.html @@ -2210,7 +2210,9 @@

Add Knowledge

: ''; // Tags for curated content - const tags = doc.relevance_tags?.length ? doc.relevance_tags.join(', ') : ''; + const tags = Array.isArray(doc.relevance_tags) && doc.relevance_tags.length + ? doc.relevance_tags.map(tag => String(tag)).join(', ') + : ''; // Fetch status badge for curated content const statusBadge = isCurated && doc.fetch_status @@ -2222,14 +2224,12 @@

Add Knowledge

// Slack-specific info const slackInfo = isSlack && doc.slack_channel_name - ? `#${doc.slack_channel_name}@${doc.slack_username || 'unknown'}` + ? `#${escapeHtml(String(doc.slack_channel_name))}@${escapeHtml(String(doc.slack_username || 'unknown'))}` : ''; // URL link const urlLink = doc.source_url || doc.fetch_url || doc.slack_permalink; - const urlHtml = urlLink - ? `${isSlack ? 'View in Slack' : 'Source'}` - : ''; + const urlHtml = renderSafeHttpLink(urlLink, isSlack ? 'View in Slack' : 'Source', true); // Content preview const contentPreview = doc.summary @@ -2282,7 +2282,7 @@

${escapeHtml(doc.title)}

${contentPreview ? `
${contentPreview}
` : ''} ${addieNotes} - ${tags ? `
Tags: ${tags}
` : ''} + ${tags ? `
Tags: ${escapeHtml(tags)}
` : ''} ${actions ? `
${actions}
` : ''} @@ -2325,15 +2325,25 @@

${escapeHtml(doc.title)}

document.getElementById('resource-edit-id').value = r.id; document.getElementById('resource-title').textContent = r.title; - document.getElementById('resource-url').innerHTML = `${escapeHtml(r.source_url || r.fetch_url)}`; + const resourceUrl = r.source_url || r.fetch_url || ''; + const resourceUrlEl = document.getElementById('resource-url'); + const safeResourceLink = renderSafeHttpLink(resourceUrl, resourceUrl); + if (safeResourceLink) { + resourceUrlEl.innerHTML = safeResourceLink; + } else { + // Preserve legacy values for admins to diagnose without activating them. + resourceUrlEl.textContent = resourceUrl || 'No URL'; + } document.getElementById('resource-summary').textContent = r.summary || 'Not yet fetched'; // Display key insights const insightsEl = document.getElementById('resource-insights'); if (r.key_insights && r.key_insights.length > 0) { - insightsEl.innerHTML = r.key_insights.map(i => - `
${i.importance} ${escapeHtml(i.insight)}
` - ).join(''); + insightsEl.innerHTML = r.key_insights.map(i => { + const importance = ['high', 'medium', 'low'].includes(i.importance) ? i.importance : 'low'; + const badge = importance === 'high' ? 'error' : importance === 'medium' ? 'warning' : 'info'; + return `
${importance} ${escapeHtml(i.insight)}
`; + }).join(''); } else { insightsEl.textContent = 'No insights yet'; } @@ -2735,8 +2745,8 @@

${escapeHtml(doc.title)}

if (data.tags && data.tags.length > 0) { tagsContainer.innerHTML = data.tags.map(t => `
- ${t.tag.replace('_', ' ')} - ${t.count} + ${escapeHtml(String(t.tag ?? '').replace('_', ' '))} + ${escapeHtml(String(t.count ?? ''))}
`).join(''); } else { @@ -2927,6 +2937,31 @@

${escapeHtml(doc.title)}

return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); } + function getSafeHttpUrl(value) { + if (typeof value !== 'string' || !value.trim()) return null; + try { + const parsed = new URL(value.trim()); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null; + if (parsed.username || parsed.password) return null; + return parsed.href; + } catch { + return null; + } + } + + function renderSafeHttpLink(value, label, stopPropagation = false) { + const safeUrl = getSafeHttpUrl(value); + if (!safeUrl) return ''; + const anchor = document.createElement('a'); + anchor.href = safeUrl; + anchor.target = '_blank'; + anchor.rel = 'noopener noreferrer'; + anchor.textContent = String(label || safeUrl); + anchor.style.color = 'var(--color-brand)'; + if (stopPropagation) anchor.setAttribute('onclick', 'event.stopPropagation();'); + return anchor.outerHTML; + } + function copyThreadId(el) { const threadId = el.dataset.threadId; navigator.clipboard.writeText(threadId); @@ -3126,7 +3161,10 @@

${isContentSuggestion ? 'Content Description' : 'Suggested Content'}

External Sources to Reference

@@ -3438,17 +3476,21 @@

Pattern Identified

const linksEl = document.getElementById('extracted-links'); if (extractedLinks.length > 0) { linksEl.style.display = 'block'; - document.getElementById('links-list').innerHTML = extractedLinks.map(link => ` + const safeExtractedLinks = extractedLinks.flatMap(link => { + const safeUrl = getSafeHttpUrl(link.url); + return safeUrl ? [{ ...link, url: safeUrl }] : []; + }); + document.getElementById('links-list').innerHTML = safeExtractedLinks.map(link => ` `).join(''); // Populate URL dropdown for perspectives form const urlSelect = document.getElementById('perspectives-url'); urlSelect.innerHTML = '' + - extractedLinks.map(link => ``).join(''); + safeExtractedLinks.map(link => ``).join(''); } else { linksEl.style.display = 'none'; } @@ -3546,7 +3588,7 @@

Pattern Identified

${msg.rating >= 3 ? '๐Ÿ‘' : '๐Ÿ‘Ž'} (${escapeHtml(msg.rating_source || 'unknown')}) ${msg.rating_notes ? `"${escapeHtml(msg.rating_notes.substring(0, 30))}${msg.rating_notes.length > 30 ? '...' : ''}"` : ''} - ${msg.feedback_tags && msg.feedback_tags.length > 0 ? `[${msg.feedback_tags.join(', ')}]` : ''} + ${msg.feedback_tags && msg.feedback_tags.length > 0 ? `[${escapeHtml(msg.feedback_tags.map(tag => String(tag)).join(', '))}]` : ''} ` : ''; @@ -3570,7 +3612,7 @@

Pattern Identified

+ placeholder="What's good or bad about this response?">${escapeHtml(msg.rating_notes || '')}
@@ -3704,6 +3746,46 @@

Pattern Identified

`; } + function linkifyBareUrls(html) { + const template = document.createElement('template'); + template.innerHTML = html; + const walker = document.createTreeWalker(template.content, 4); + const textNodes = []; + let node; + + while ((node = walker.nextNode())) textNodes.push(node); + + textNodes.forEach(textNode => { + if (textNode.parentElement?.closest('a, code, pre')) return; + + const value = textNode.nodeValue || ''; + const urlPattern = /https?:\/\/[^\s<>"]+/g; + let match; + let cursor = 0; + let fragment = null; + + while ((match = urlPattern.exec(value))) { + fragment ||= document.createDocumentFragment(); + fragment.append(document.createTextNode(value.slice(cursor, match.index))); + + const anchor = document.createElement('a'); + anchor.href = match[0]; + anchor.target = '_blank'; + anchor.rel = 'noopener noreferrer'; + anchor.textContent = match[0]; + fragment.append(anchor); + cursor = match.index + match[0].length; + } + + if (fragment) { + fragment.append(document.createTextNode(value.slice(cursor))); + textNode.replaceWith(fragment); + } + }); + + return template.innerHTML; + } + function renderMarkdown(text) { // Replace Slack user mentions with display names before escaping HTML // Format: <@U1234567> -> @DisplayName @@ -3713,7 +3795,11 @@

Pattern Identified

}); // Escape HTML first - text = text.replace(/&/g, '&').replace(//g, '>'); + text = text.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); // Code blocks text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, '
$2
'); @@ -3728,10 +3814,15 @@

Pattern Identified

text = text.replace(/\*([^*]+)\*/g, '$1'); // Links - make them clickable - text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - - // Bare URLs (not already in markdown links) - text = text.replace(/(?)(https?:\/\/[^\s<>"]+)/g, '$1'); + text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, href) => { + try { + const parsed = new URL(href.replace(/&/g, '&')); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return label; + return `${label}`; + } catch { + return label; + } + }); // Line breaks to paragraphs const paragraphs = text.split(/\n\n+/); @@ -3750,7 +3841,8 @@

Pattern Identified

return '

' + p.replace(/\n/g, '
') + '

'; }).join(''); - return text; + // Linkify text nodes only, so generated Markdown/code HTML is never reprocessed. + return linkifyBareUrls(text); } function renderExecutionPlan(toolExecutions, totalLatency) { diff --git a/server/public/admin-brands.html b/server/public/admin-brands.html index b0a64dd6a8..3e532d777a 100644 --- a/server/public/admin-brands.html +++ b/server/public/admin-brands.html @@ -726,17 +726,24 @@

Brand Details

} } + function getSafeResearchLogoUrl(value) { + if (typeof value !== 'string' || value.length > 2048) return null; + try { + const parsed = new URL(value); + if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return null; + return parsed.href; + } catch { + return null; + } + } + function displayResearchResults(data) { const container = document.getElementById('researchContent'); const manifest = data.manifest || {}; + const safeLogoUrl = getSafeResearchLogoUrl(manifest.logos?.[0]?.url); let html = '
'; - // Logo - if (manifest.logos && manifest.logos.length > 0) { - html += `Logo`; - } - html += `
${escapeHtml(manifest.name || data.domain)}
@@ -768,6 +775,15 @@

Brand Details

} container.innerHTML = html; + if (safeLogoUrl) { + const logo = document.createElement('img'); + logo.src = safeLogoUrl; + logo.alt = 'Logo'; + logo.addEventListener('error', () => { + logo.style.display = 'none'; + }); + container.querySelector('.brand-preview')?.prepend(logo); + } document.getElementById('researchResults').style.display = 'block'; document.getElementById('researchError').style.display = 'none'; } @@ -886,19 +902,25 @@

Brand Details

async function viewBrand(domain) { document.getElementById('viewModal').style.display = 'block'; document.getElementById('viewModalTitle').textContent = domain; - document.getElementById('viewContent').innerHTML = '
Loading...
'; + const viewContent = document.getElementById('viewContent'); + viewContent.innerHTML = '
Loading...
'; try { const response = await fetch(`/api/brands/resolve?domain=${encodeURIComponent(domain)}`); const data = await response.json(); - let html = '
';
-        html += JSON.stringify(data, null, 2);
-        html += '
'; - - document.getElementById('viewContent').innerHTML = html; + const preview = document.createElement('pre'); + preview.style.background = 'var(--color-bg-subtle)'; + preview.style.padding = 'var(--space-4)'; + preview.style.borderRadius = 'var(--radius-md)'; + preview.style.overflow = 'auto'; + preview.textContent = JSON.stringify(data, null, 2); + viewContent.replaceChildren(preview); } catch (error) { - document.getElementById('viewContent').innerHTML = `
${escapeHtml(error.message)}
`; + const errorMessage = document.createElement('div'); + errorMessage.className = 'error-message'; + errorMessage.textContent = error.message; + viewContent.replaceChildren(errorMessage); } } diff --git a/server/public/admin-feeds.html b/server/public/admin-feeds.html index 068488cdd0..4917e819d7 100644 --- a/server/public/admin-feeds.html +++ b/server/public/admin-feeds.html @@ -7,6 +7,7 @@ Admin - Industry Feeds - AdCP Registry +