Die HoneyBar braucht kein 'unsafe-inline' mehr

Ihre Stile hingen als <style> im Shadow DOM, und Markup faellt unter die
CSP der Gastgeber-App: Jede fremde App musste deshalb style-src
'unsafe-inline' erlauben, sonst stand die HoneyBar schwarz auf schwarz da.

Jetzt kommen sie als konstruiertes Stylesheet ueber die CSSOM herein, die
keine CSP kennt. Damit braucht die App gar keine style-src-Erlaubnis mehr
-- auch keine fuer die HoneyOS-Domain, was der geplante Weg ueber eine
zweite Datei und <link> gekostet haette, samt zweiter Anfrage.

Der Strich zum Wiederausklappen hat seinen eigenen Shadow-Root und geht
denselben Weg. Der Rueckfall fuer alte Browser laeuft nach innerHTML,
sonst raeumt das Markup ihn gleich wieder weg.

Gemessen an einer nachgebauten App mit style-src 'self': Leiste bernstein,
32 px, Strich mit Verlauf, keine CSP-Meldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-11 13:50:06 +02:00
1 parent 8ca16aec84
commit 3b47003492
4 files changed
+97 -33

No files matched your search

+17 -12
View File
@@ -288,21 +288,26 @@ Zwei Auswege:
→ Ersteres ist das Richtige, sobald die HoneyBar mehr kann als heute (Widgets,
Systemmenü). Vorher lohnt der Umbau nicht.
### Die HoneyBar ohne `unsafe-inline`
### Die HoneyBar ohne `unsafe-inline` — ✅ gebaut (11.09.2026)
Fremde Apps bekommen die HoneyBar über den Reverse Proxy (siehe README). Dabei
muss deren CSP heute `style-src 'unsafe-inline'` erlauben, weil die HoneyBar ihre
Stile im Shadow DOM mitbringt — ohne das erscheint sie schwarz auf schwarz.
Gemessen am 28.08.2026 an einer nachgebauten fremden App.
Fremde Apps mussten für die HoneyBar `style-src 'unsafe-inline'` erlauben, weil
sie ihre Stile im Shadow DOM als `<style>` mitbrachte — ohne das erschien sie
schwarz auf schwarz (gemessen am 28.08.2026).
**Auflösbar:** Die Stile als eigene Datei neben `honeybar.js` ausliefern und im
Shadow DOM per `<link>` einbinden. Dann genügt `style-src 'self' https://honeyos…`,
und die CSP der fremden App bleibt streng. Kostet eine zweite Anfrage, die
zwischengespeichert wird.
**Anders gelöst als hier geplant.** Der Plan war, die Stile als eigene Datei
auszuliefern und per `<link>` einzubinden. Das hätte `'unsafe-inline'` gegen
`style-src … https://honeyos…` getauscht — eine CSP-Änderung gegen eine andere,
plus eine zweite Anfrage. Stattdessen hängt die HoneyBar ihre Stile jetzt als
konstruiertes Stylesheet (`adoptedStyleSheets`) ein: Die CSP greift nur auf
Markup, nicht auf die CSSOM. Damit braucht die Gastgeber-App **gar keine
`style-src`-Erlaubnis** mehr, und es bleibt bei einer Datei ohne zweite Anfrage.
Zu prüfen ist dabei, was sonst noch inline gesetzt wird: die Lage der HoneyBar
selbst und der Innenabstand, den sie der Seite verpasst. Über die CSSOM gesetzte
Stile (`el.style.x = …`) fallen nicht unter die CSP — was im Markup steht, schon.
Der Strich zum Wiederausklappen hat seinen eigenen Shadow-Root und wäre sonst
das einzige Stück ohne Farbe geblieben — er geht denselben Weg.
Zwei Fallen dabei: Der Rückfall für alte Browser hängt ein `<style>` in den
Root, also muss er **nach** `innerHTML` laufen, sonst räumt das Markup ihn
gleich wieder weg. Und `el.style.…` war nie das Problem — das ist schon CSSOM.
### Bild-Widget — ✅ gebaut (30.08.2026)
+13 -7
View File
@@ -448,7 +448,7 @@ location / {
# 2. Die CSP der App ersetzen, nicht ergänzen. Zwei CSP-Kopfzeilen bedeuten
# nicht „mehr erlaubt", sondern „beide müssen zustimmen".
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://honeyos.example.com; connect-src 'self' https://honeyos.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://honeyos.example.com; connect-src 'self' https://honeyos.example.com; style-src 'self'; img-src 'self' data:" always;
# 3. Nur HTML anfassen; JSON, Bilder und Downloads bleiben unberührt.
sub_filter_types text/html;
@@ -463,12 +463,18 @@ Liste ist zugleich die CORS-Freigabe. **Auf den Ursprung genau**: `localhost` un
oder ein abweichender Port. Passt er nicht, erscheint die HoneyBar mit leerer
App-Liste und in der Konsole steht ein CORS-Fehler.
**Warum `style-src 'unsafe-inline'`:** Die HoneyBar bringt ihre Stile mit, im
Shadow DOM. Eine strenge CSP verbietet die — gemessen: die HoneyBar erscheint,
aber schwarz auf schwarz, ohne Röhre. Das ist eine echte Abschwächung der
CSP dieser App; viele fertige Werkzeuge brauchen `unsafe-inline` ohnehin für
sich selbst, aber wissen sollte man es. (Auflösbar, indem die HoneyBar ihre Stile
als eigene Datei ausliefert — steht in [IDEAS.md](IDEAS.md).)
**Kein `style-src 'unsafe-inline'` nötig** (seit 11.09.2026). Die HoneyBar
bringt ihre Stile zwar mit, hängt sie aber als konstruiertes Stylesheet
(`adoptedStyleSheets`) in ihren Shadow DOM statt als `<style>`-Element. Nur
Markup fällt unter die CSP, über die CSSOM gesetzte Stile nicht — die App
braucht für die HoneyBar also weder `'unsafe-inline'` noch eine Freigabe für
die HoneyOS-Domain. Vorher stand die HoneyBar unter einer strengen CSP schwarz
auf schwarz da. Gemessen an einer nachgebauten App mit `style-src 'self'`:
Leiste bernstein, 32 px, keine CSP-Meldung.
Browser ohne konstruierbare Stylesheets (vor Safari 16.4, Firefox 101) fallen
auf das `<style>`-Element zurück — dort gilt der alte Stand, also `unsafe-inline`
oder eine Leiste ohne Farbe.
**Apps mit COEP** (alles mit WASM oder SharedArrayBuffer — BentoPDF etwa)
lehnen fremde Dateien ab, die sich nicht zur Einbettung bekennen. HoneyOS
+46 -14
View File
@@ -105,10 +105,37 @@
*/
const WAPPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 12" shape-rendering="crispEdges" aria-hidden="true"><rect x="4" y="0" width="2" height="1" fill="#f5f2e7"/><rect x="9" y="0" width="2" height="1" fill="#f5f2e7"/><rect x="2" y="1" width="5" height="1" fill="#f5f2e7"/><rect x="8" y="1" width="5" height="1" fill="#f5f2e7"/><rect x="2" y="2" width="11" height="1" fill="#f5f2e7"/><rect x="14" y="2" width="1" height="1" fill="#3b2412"/><rect x="3" y="3" width="9" height="1" fill="#f5f2e7"/><rect x="13" y="3" width="2" height="1" fill="#3b2412"/><rect x="3" y="4" width="11" height="1" fill="#ffd060"/><rect x="2" y="5" width="2" height="1" fill="#f2b33d"/><rect x="4" y="5" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="5" width="2" height="1" fill="#f2b33d"/><rect x="8" y="5" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="5" width="5" height="1" fill="#f2b33d"/><rect x="1" y="6" width="3" height="1" fill="#f2b33d"/><rect x="4" y="6" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="6" width="2" height="1" fill="#f2b33d"/><rect x="8" y="6" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="6" width="2" height="1" fill="#f2b33d"/><rect x="12" y="6" width="1" height="1" fill="#181410"/><rect x="13" y="6" width="1" height="1" fill="#63c7c0"/><rect x="14" y="6" width="1" height="1" fill="#f2b33d"/><rect x="0" y="7" width="1" height="1" fill="#5c3a1c"/><rect x="1" y="7" width="3" height="1" fill="#f2b33d"/><rect x="4" y="7" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="7" width="2" height="1" fill="#f2b33d"/><rect x="8" y="7" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="7" width="2" height="1" fill="#f2b33d"/><rect x="12" y="7" width="2" height="1" fill="#181410"/><rect x="14" y="7" width="1" height="1" fill="#f2b33d"/><rect x="1" y="8" width="3" height="1" fill="#f2b33d"/><rect x="4" y="8" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="8" width="2" height="1" fill="#f2b33d"/><rect x="8" y="8" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="8" width="5" height="1" fill="#f2b33d"/><rect x="2" y="9" width="1" height="1" fill="#d2952a"/><rect x="3" y="9" width="2" height="1" fill="#462b14"/><rect x="5" y="9" width="2" height="1" fill="#d2952a"/><rect x="7" y="9" width="2" height="1" fill="#462b14"/><rect x="9" y="9" width="6" height="1" fill="#d2952a"/><rect x="3" y="10" width="2" height="1" fill="#181410"/><rect x="7" y="10" width="2" height="1" fill="#181410"/><rect x="11" y="10" width="2" height="1" fill="#181410"/><rect x="3" y="11" width="2" height="1" fill="#181410"/><rect x="7" y="11" width="2" height="1" fill="#181410"/><rect x="11" y="11" width="2" height="1" fill="#181410"/></svg>';
/* ── Stile ohne CSP-Ausnahme ──────────────────────────────────────────────
* Die Stile kommen als konstruiertes Stylesheet in den Shadow-Root, nicht
* als style-Element. Der Unterschied ist nicht Geschmack, sondern der
* ganze Punkt: Ein style-Element ist Markup und faellt damit unter die CSP der
* fremden App — die musste deshalb bis hierher style-src 'unsafe-inline'
* erlauben, sonst stand die HoneyBar schwarz auf schwarz da. Ueber die
* CSSOM gesetzte Stile fallen nicht darunter. So braucht die Gastgeber-App
* fuer die HoneyBar gar keine style-src-Erlaubnis mehr, und es bleibt
* trotzdem bei einer Datei ohne zweite Anfrage.
*
* Der Rueckfall ist fuer Browser ohne konstruierbare Stylesheets (vor
* Safari 16.4, Firefox 101): Dort steht es wie bisher, also nicht
* schlechter — nur eben weiter mit 'unsafe-inline'.
*/
function stileAnlegen(wurzel, css) {
try {
const blatt = new CSSStyleSheet();
blatt.replaceSync(css);
wurzel.adoptedStyleSheets = [blatt];
return;
} catch {
/* alter Browser: unten der Weg ueber das Markup */
}
const el = document.createElement("style");
el.textContent = css;
wurzel.appendChild(el);
}
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
const STIL = `
:host, * { box-sizing: border-box; }
/* Dieselben Röhren wie auf dem Schreibtisch. Welche gilt, sagt der
Server; bis seine Antwort da ist, leuchtet Bernstein. */
@@ -253,8 +280,9 @@
Logo links, das App-Menue am Pfeil rechts. */
.menu.sys { left: 6px; right: auto; }
@media (max-width: 560px) { .clock { display: none; } }
</style>
`;
root.innerHTML = `
<div class="bar">
<button class="home" id="home" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false"><span class="hex">${WAPPEN}</span> HONEY<b>OS</b></button>
<nav class="apps" id="apps"></nav>
@@ -277,6 +305,8 @@
<div class="drawer" id="menuApps"></div>
</div>`;
stileAnlegen(root, STIL);
/* ── Aussehen ─────────────────────────────────────────────────────────────
* Dieselben drei Röhren wie in style.css. Sie hier zu wiederholen ist die
* bewusste Entscheidung: die HoneyBar soll ohne zweite Datei auskommen, damit
@@ -520,17 +550,19 @@
// auch wenn in HoneyOS Grün eingestellt war. Er gehört zur HoneyBar, also
// trägt er dieselbe Röhre.
const t = ROEHREN[(settings && settings.phosphor) || "amber"] || ROEHREN.amber;
r.innerHTML = `
<style>
.strich {
height: 3px;
background: linear-gradient(90deg, ${t.dim}, ${t.p} 50%, ${t.dim});
opacity: .55;
transition: opacity .15s, height .15s;
}
:host(:hover) .strich { opacity: 1; height: 3px; }
</style>
<div class="strich"></div>`;
// Auch hier ueber die CSSOM statt als style-Element: Der Strich hat seinen
// eigenen Shadow-Root und waere sonst das einzige Stueck geblieben, das
// unter einer strengen CSP unsichtbar bleibt.
r.innerHTML = `<div class="strich"></div>`;
stileAnlegen(r, `
.strich {
height: 3px;
background: linear-gradient(90deg, ${t.dim}, ${t.p} 50%, ${t.dim});
opacity: .55;
transition: opacity .15s, height .15s;
}
:host(:hover) .strich { opacity: 1; height: 3px; }
`);
nub.addEventListener("click", () => {
forget();
+21
View File
@@ -204,3 +204,24 @@ func TestWappenIstUeberallDasselbe(t *testing.T) {
}
}
}
// Die HoneyBar darf ihre Stile nicht als <style>-Element mitbringen: Markup
// faellt unter die CSP der fremden App, ueber die CSSOM gesetzte Stile nicht.
// Mit einem <style> muesste jede Gastgeber-App wieder 'unsafe-inline' erlauben,
// und ohne das stuende die HoneyBar dort schwarz auf schwarz — genau so war es
// bis zum 11.09.2026. Der Rueckfall fuer alte Browser baut das Element per
// createElement, nicht im Markup; deshalb sucht der Test nach dem Tag.
func TestHoneyBarBringtKeinStyleElementMit(t *testing.T) {
b, err := os.ReadFile("web/honeybar/honeybar.js")
if err != nil {
t.Fatal(err)
}
src := string(b)
if strings.Contains(src, "<style>") || strings.Contains(src, "<style ") {
t.Error("honeybar.js enthaelt ein <style>-Element im Markup — das braucht 'unsafe-inline' in der fremden App")
}
if !strings.Contains(src, "adoptedStyleSheets") {
t.Error("honeybar.js setzt keine adoptedStyleSheets mehr — wie kommen die Stile in den Shadow DOM?")
}
}