fix(ui): render sanitized HTML in playlist descriptions

This commit is contained in:
vicrodh committed 2026-02-22 17:58:41 -06:00
1 parent 66d25b773b
commit 37cca5dccf
2 files changed
+48 -1

No files matched your search

@@ -22,6 +22,7 @@
import { isTrackUnavailable, clearTrackUnavailable, subscribe as subscribeUnavailable } from '$lib/stores/unavailableTracksStore';
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
import { showToast } from '$lib/stores/toastStore';
import { sanitizeHtml } from '$lib/utils/sanitize';
import { get } from 'svelte/store';
import { t } from '$lib/i18n';
import { onMount, tick } from 'svelte';
@@ -2020,7 +2021,7 @@
<span class="playlist-label">Playlist</span>
<h1 class="playlist-title">{playlist.name}</h1>
{#if playlist.description}
<p class="playlist-description">{playlist.description}</p>
<p class="playlist-description">{@html sanitizeHtml(playlist.description)}</p>
{/if}
<div class="playlist-info">
<span class="owner">{playlist.owner.name}</span>
@@ -2603,6 +2604,15 @@
line-height: 1.4;
}
.playlist-description :global(a) {
color: var(--accent-primary);
text-decoration: none;
}
.playlist-description :global(a:hover) {
text-decoration: underline;
}
.playlist-info {
display: flex;
align-items: center;
+37
View File
@@ -0,0 +1,37 @@
/**
* Lightweight HTML sanitizer for rendering API content (e.g. Qobuz playlist descriptions).
* Allows safe tags and strips dangerous elements/attributes.
*/
const ALLOWED_TAGS = new Set([
'b', 'i', 'em', 'strong', 'br', 'a', 'p', 'span', 'ul', 'ol', 'li',
]);
/** Strip event handler attributes and javascript: URLs */
function stripDangerousAttrs(tag: string): string {
// Remove on* event attributes
tag = tag.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
// Remove javascript: URLs from href/src
tag = tag.replace(/(href|src)\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, '$1=""');
return tag;
}
export function sanitizeHtml(html: string): string {
if (!html) return '';
// Remove <script> and <iframe> blocks entirely (with content)
let result = html.replace(/<script[\s\S]*?<\/script>/gi, '');
result = result.replace(/<iframe[\s\S]*?<\/iframe>/gi, '');
result = result.replace(/<style[\s\S]*?<\/style>/gi, '');
// Process remaining tags: keep allowed, strip others
result = result.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
const lower = tagName.toLowerCase();
if (ALLOWED_TAGS.has(lower)) {
return stripDangerousAttrs(match);
}
return '';
});
return result;
}