From ee4c5d85b0665bebe14c7ddac25ec99cd3a36ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Rolf?= Date: Wed, 7 Oct 2026 23:16:59 +0200 Subject: [PATCH] fix(ui): parse HTML in an inert DOMParser document in stripHtml/isEmptyHtml (#20) innerHTML on an element of the live document loads images and runs inline handlers (), even when detached. DOMParser documents execute and fetch nothing. Behaviour otherwise unchanged. Bump ui to 0.7.2; move the released 0.7.1 notes out of [Unreleased]. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 37 +++++++++++++++++++++++------------ packages/ui/package-lock.json | 4 ++-- packages/ui/package.json | 2 +- packages/ui/src/html.ts | 19 +++++++++++------- 4 files changed, 40 insertions(+), 22 deletions(-) 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"); }