Skip to content

Commit fbfd63c

Browse files
rdimitrovclaude
andauthored
fix(ui): encode publisher-controlled URLs for href attribute context (#1248)
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) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7a4216c commit fbfd63c

1 file changed

Lines changed: 16 additions & 2 deletions

File tree

internal/api/handlers/v0/ui_index.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,13 +423,13 @@ <h4 class="font-medium text-gray-900 mb-2">Remotes</h4>
423423
${server.repository?.url ? `
424424
<div>
425425
<span class="font-medium">Repository:</span>
426-
<a href="${escapeHtml(server.repository.url)}" target="_blank" class="text-blue-600 hover:underline break-all" onclick="event.stopPropagation()">${escapeHtml(server.repository.url)}</a>
426+
<a href="${escapeAttr(server.repository.url)}" target="_blank" class="text-blue-600 hover:underline break-all" onclick="event.stopPropagation()">${escapeHtml(server.repository.url)}</a>
427427
</div>
428428
` : ''}
429429
${server.websiteUrl ? `
430430
<div>
431431
<span class="font-medium">Website:</span>
432-
<a href="${escapeHtml(server.websiteUrl)}" target="_blank" class="text-blue-600 hover:underline break-all" onclick="event.stopPropagation()">${escapeHtml(server.websiteUrl)}</a>
432+
<a href="${escapeAttr(server.websiteUrl)}" target="_blank" class="text-blue-600 hover:underline break-all" onclick="event.stopPropagation()">${escapeHtml(server.websiteUrl)}</a>
433433
</div>
434434
` : ''}
435435
<div>
@@ -491,12 +491,26 @@ <h4 class="font-medium text-gray-900 mb-2">Remotes</h4>
491491
});
492492

493493
// Utility functions
494+
// Safe for element-text contexts only: encodes `&`, `<`, `>`. Does NOT
495+
// encode `"` or `'`, so it is unsafe inside attribute values — use
496+
// escapeAttr there.
494497
function escapeHtml(text) {
495498
const div = document.createElement('div');
496499
div.textContent = text;
497500
return div.innerHTML;
498501
}
499502

503+
// Safe for HTML attribute values per OWASP attribute-encoding rule.
504+
function escapeAttr(text) {
505+
if (text == null) return '';
506+
return String(text)
507+
.replace(/&/g, '&amp;')
508+
.replace(/</g, '&lt;')
509+
.replace(/>/g, '&gt;')
510+
.replace(/"/g, '&quot;')
511+
.replace(/'/g, '&#39;');
512+
}
513+
500514
function formatDate(dateStr) {
501515
if (!dateStr) return '-';
502516
const date = new Date(dateStr);

0 commit comments

Comments
 (0)