diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72e5257..604bbbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,20 +7,16 @@ Changelog ist die Upgrade-Anleitung für die Tools.
## [Unreleased]
-### @basicbar/ui (→ wird `ui/v0.7.1`)
+### @basicbar/ui (→ wird `ui/v0.7.2`)
-**A11y-Fix `TranslatableField`** (#11): Der `hint` hatte mit
-`text-slate-400` nur ~2,6:1 Kontrast und war nicht mit dem Feld verknüpft.
-Jetzt `text-slate-600 dark:text-slate-300` (AA), und Hinweis,
-Veraltet-Hinweis und Pflichtfeld-Fehler hängen per `aria-describedby` an der
-Eingabe (Screenreader lesen sie mit dem Feld vor); Veraltet- und
-Fehlertext ebenfalls auf AA-Kontrast (`amber-700`, `rose-600`).
-`renderInput` bekommt dafür `describedBy`, `RichTextEditor` eine gleichnamige
-Prop (auch zur Laufzeit aktualisiert).
+**Sicherheits-Fix `stripHtml` / `isEmptyHtml`** (#20): Beide parsten über
+`innerHTML` auf einem Element des Live-Dokuments — dort lädt der Browser
+Bilder und führt Inline-Handler aus, `
` lief also, auch
+ohne Einhängen. Jetzt wird in einem inerten `DOMParser`-Dokument geparst
+(nichts wird ausgeführt oder geladen); `isEmptyHtml` erkennt Bilder über den
+geparsten Body statt per Regex. Verhalten sonst unverändert.
-Migration: Tarball auf `ui/v0.7.1`. Wer `renderInput` mit `RichTextEditor`
-nutzt, reicht zusätzlich `describedBy={describedBy}` durch (sonst bleibt der
-Editor ohne Beschreibung — nichts bricht).
+Migration: Tarball auf `ui/v0.7.2`. Keine Code-Änderung nötig.
### basicbar-integrations (→ wird `integrations/v0.3.0`)
@@ -53,6 +49,23 @@ Migration.
- Doku-Drift zum Capabilities-Endpunkt in CLAUDE.md und EXTRAKTIONSPLAN
bereinigt.
+## ui/v0.7.1 — 2026-10-02
+
+### @basicbar/ui
+
+**A11y-Fix `TranslatableField`** (#11): Der `hint` hatte mit
+`text-slate-400` nur ~2,6:1 Kontrast und war nicht mit dem Feld verknüpft.
+Jetzt `text-slate-600 dark:text-slate-300` (AA), und Hinweis,
+Veraltet-Hinweis und Pflichtfeld-Fehler hängen per `aria-describedby` an der
+Eingabe (Screenreader lesen sie mit dem Feld vor); Veraltet- und
+Fehlertext ebenfalls auf AA-Kontrast (`amber-700`, `rose-600`).
+`renderInput` bekommt dafür `describedBy`, `RichTextEditor` eine gleichnamige
+Prop (auch zur Laufzeit aktualisiert).
+
+Migration: Tarball auf `ui/v0.7.1`. Wer `renderInput` mit `RichTextEditor`
+nutzt, reicht zusätzlich `describedBy={describedBy}` durch (sonst bleibt der
+Editor ohne Beschreibung — nichts bricht).
+
## auth/v0.2.0 — 2026-10-01
### basicbar-auth
diff --git a/packages/ui/package-lock.json b/packages/ui/package-lock.json
index a565996..43a6b71 100644
--- a/packages/ui/package-lock.json
+++ b/packages/ui/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@basicbar/ui",
- "version": "0.7.1",
+ "version": "0.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@basicbar/ui",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "Apache-2.0",
"dependencies": {
"@tiptap/extension-bold": "^3.27.2",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index c9142ae..88e2124 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@basicbar/ui",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "Design-System-Basis der virtUOS -bar-Tools: Tailwind-Preset, Basis-Styles, Theme (Dark Mode), i18n-Bootstrap, contentLang, TranslatableField, Preferences, RichText; RichTextEditor (TipTap) als eigener Entry",
"license": "Apache-2.0",
"author": "Universität Osnabrück (virtUOS)",
diff --git a/packages/ui/src/html.ts b/packages/ui/src/html.ts
index 78e8e81..e29ffca 100644
--- a/packages/ui/src/html.ts
+++ b/packages/ui/src/html.ts
@@ -3,15 +3,19 @@
/** Small helpers for the HTML that rich-text fields store. They parse via the
* DOM (not a regex), so entities and nested markup are handled like the
- * browser handles them; nothing is executed — `innerHTML` on a detached
- * element never runs scripts or loads resources synchronously. */
+ * browser handles them. Parsing happens in an inert `DOMParser` document:
+ * unlike `innerHTML` on an element of the live document — which loads images
+ * and runs inline handlers such as `
` even when detached —
+ * nothing in the input is executed or fetched (basicbar#20). */
+
+function parseBody(html: string): HTMLElement {
+ return new DOMParser().parseFromString(html, "text/html").body;
+}
/** The visible text of an HTML fragment, whitespace-trimmed. */
export function stripHtml(html: string): string {
if (!html) return "";
- const div = document.createElement("div");
- div.innerHTML = html;
- return div.textContent?.trim() ?? "";
+ return parseBody(html).textContent?.trim() ?? "";
}
/** True when a stored rich-text value carries no content: no visible text and
@@ -19,6 +23,7 @@ export function stripHtml(html: string): string {
* behind — that is empty; an image-only caption is not. */
export function isEmptyHtml(html: string | null | undefined): boolean {
if (!html) return true;
- if (stripHtml(html)) return false;
- return !/
]/i.test(html);
+ const body = parseBody(html);
+ if (body.textContent?.trim()) return false;
+ return !body.querySelector("img");
}