From fbfd63ced8e1f6bee9ccd0dd93f1b8fe087f9f22 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Mon, 4 May 2026 09:25:41 -0400 Subject: [PATCH] fix(ui): encode publisher-controlled URLs for href attribute context (#1248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalogue page interpolates publisher-supplied URL fields (`server.repository.url`, `server.websiteUrl`) inside `href="..."` attributes via `innerHTML`, escaping them with a `textContent`→`innerHTML` helper. Per the HTML5 fragment-serialisation algorithm, that round-trip encodes only `&`, `<`, `>`, and U+00A0 inside text nodes — it does not encode `"` or `'`. The helper is therefore safe in element-text contexts (where it is used for `name`, `version`, `description`, etc.) but unsafe inside an attribute value. Add an attribute-safe `escapeAttr` helper that follows the OWASP attribute-encoding rule (also encodes `"` and `'`) and use it for the two `href` interpolations. Element-text usages keep the existing `escapeHtml` and remain unchanged. A short comment on each helper documents the contexts they cover so future fields are wired up correctly. Co-Authored-By: Claude Opus 4.7 (1M context) Co-authored-by: Claude Opus 4.7 (1M context) --- internal/api/handlers/v0/ui_index.html | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/api/handlers/v0/ui_index.html b/internal/api/handlers/v0/ui_index.html index 38bac7f..dc437ff 100644 --- a/internal/api/handlers/v0/ui_index.html +++ b/internal/api/handlers/v0/ui_index.html @@ -423,13 +423,13 @@ ${server.repository?.url ? `
Repository: - ${escapeHtml(server.repository.url)} + ${escapeHtml(server.repository.url)}
` : ''} ${server.websiteUrl ? `
Website: - ${escapeHtml(server.websiteUrl)} + ${escapeHtml(server.websiteUrl)}
` : ''}
@@ -491,12 +491,26 @@ }); // Utility functions + // Safe for element-text contexts only: encodes `&`, `<`, `>`. Does NOT + // encode `"` or `'`, so it is unsafe inside attribute values — use + // escapeAttr there. function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } + // Safe for HTML attribute values per OWASP attribute-encoding rule. + function escapeAttr(text) { + if (text == null) return ''; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + function formatDate(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr);