CVE-2026-112958

mediumPriorität · verfolgen · 0EPSS 0.003 · 23. PerzentilUS
grimoire priorityP3 · Track
Exploitable?
—
no known exploitation
Likely / severe?
0.30% (23rd) EPSS
CVSS 5.4 · medium
Is it mine?
1 products
Prüfe deine Angriffsfläche
Who says so?
1 sources
osv

EPSS · bewertet 2026-10-28

ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, a project-defined free-form widget style field can carry an editor-controlled quote into an inline style attribute because the wrapper serializer does not apply HTML-attribute encoding. The quote can terminate the style attribute and inject executable attributes when published content, or an affected draft viewed by a higher-privileged user, is rendered. This issue is fixed in version 4.34.0.

01

cvss

Technischer Schweregrad, 0–10. Eine Zeile je Quelle.

5.4
010

Basiswert 0–10 · höher = schwerwiegender · Vektor überfahren zum Auflösen

v3.15.4mediumCVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:Nsecondary · security-advisories@github.comAusnutzbarkeit 2.3 · Auswirkung 2.7
v3.15.4mediumCVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:Nsecondary · OSV
v3.15mediumCVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N/E:Usecondary · CVSS-BT (t0sche)
02

SSVC

Eine Handlungsentscheidung — aus Ausnutzung und Auswirkung.

Exploitation: poc · Automatable: no · Technical Impact: partialcoordinatorcisa_ssvcUS
03

schwächen

Die CWE-Schwächeklassen, zu denen dies gehört.

04

betroffene Produkte (1)

Betroffene Produkte und Versionsbereiche.

05

Advisories (1)

Hersteller- und Ökosystem-Meldungen dazu.

  • GHSA-7jg5-frgc-c67wApostropheCMS: Stored XSS via Unescaped Widget Style Attribute Serialization2026-10-21ghsa · osvUS

    ## Summary ApostropheCMS widget styles support project-defined schema fields. If a project defines a free-form `string` style field with a CSS `property` and no selector, an editor-controlled value is rendered as an inline CSS declaration. The resulting declaration is then inserted into the widget wrapper's `style="..."` attribute without HTML-attribute escaping. An attacker who can edit the widget can include a double quote in the style value, terminate the `style` attribute, and inject new HTML event-handler attributes. The payload is stored with the widget and executes in the Apostrophe site origin whenever the affected published page is rendered. This is a sibling of CVE-2026-33889 / `GHSA-97v6-998m-fp4g`. The earlier issue escaped from a `<style>` element through a malicious color value. This variant reaches a different sink related to the automatically generated widget wrapper's inline `style` and `class` attributes. ## Details ### Root Cause 1: Free-form string style values retain HTML metacharacters The standard string field converter uses `launder.string()` and length validation. This performs type normalization, not output encoding for an HTML attribute context: [`packages/apostrophe/modules/@apostrophecms/schema/lib/addFieldTypes.js:296-333`](https://github.com/apostrophecms/apostrophe/blob/aa5170c7df9ff9ea5ac2e72a008f12e28dae6ca8/packages/apostrophe/modules/%40apostrophecms/schema/lib/addFieldTypes.js#L296-L333) ```javascript 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 ); } }); ``` Quotes therefore remain in a value supplied through a style field. ### Root Cause 2: The value is converted into inline CSS without value validation For a field with `property` but no `selector` or media query, `renderScopedStyles()` selects inline rendering. The generic extractor concatenates the configured property and editor-controlled value: [`packages/apostrophe/modules/@apostrophecms/styles/ui/apos/universal/render.mjs:171-228`](https://github.com/apostrophecms/apostrophe/blob/aa5170c7df9ff9ea5ac2e72a008f12e28dae6ca8/packages/apostrophe/modules/%40apostrophecms/styles/ui/apos/universal/render.mjs#L171-L228) [`packages/apostrophe/modules/@apostrophecms/styles/ui/apos/universal/render.mjs:598-647`](https://github.com/apostrophecms/apostrophe/blob/aa5170c7df9ff9ea5ac2e72a008f12e28dae6ca8/packages/apostrophe/modules/%40apostrophecms/styles/ui/apos/universal/render.mjs#L598-L647) ```javascript rule = `${property}: ${normalized.value}${normalized.unit}`; // ... if (isInline) { return { css: '', classes: [ ...storage.classes ], inline: stringifyRules(storage.styles, true) }; } ``` For example, the value below becomes part of a `font-family` declaration: ```text serif" tabindex="0" autofocus onfocus="document.body.dataset.inline='executed' ``` ### Root Cause 3: Attribute values are serialized without HTML escaping `getWidgetAttributes()` places the inline CSS into an attribute tuple, then concatenates every tuple into `key="value"` without encoding `&`, `<`, `>`, `'`, or `"`: [`packages/apostrophe/modules/@apostrophecms/styles/lib/methods.js:251-322`](https://github.com/apostrophecms/apostrophe/blob/aa5170c7df9ff9ea5ac2e72a008f12e28dae6ca8/packages/apostrophe/modules/%40apostrophecms/styles/lib/methods.js#L251-L322) ```javascript if (styleParts.length) { attrs.push( [ 'style', `${styleParts.join(';')};` ] ); } if (options.asObject) { return Object.fromEntries(attrs); } return attrs .map(([ key, value ]) => `${key}="${value}"`) .join(' '); ``` The same raw serializer also makes a project-defined free-form `string` style with `class: true` a potential sibling path, because the string is added to `classes` and later serialized into `class="..."`. The inline-property path was the path dynamically verified. ### Root Cause 4: The unsafe string is used by the automatic public wrapper Styled widgets use `getWidgetAttributes()` to create a wrapper around the rendered widget: [`packages/apostrophe/modules/@apostrophecms/widget-type/index.js:448-457`](https://github.com/apostrophecms/apostrophe/blob/aa5170c7df9ff9ea5ac2e72a008f12e28dae6ca8/packages/apostrophe/modules/%40apostrophecms/widget-type/index.js#L448-L457) ```javascript 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>`; } ``` Consequently, the injected attribute is present on normal public page renders, not only in the editor UI. ### Why the CVE-2026-33889 defense does not prevent this variant The sibling advisory led to a defense in `getWidgetElements()` that rewrites `</` before CSS is inserted into a `<style>` element: ```javascript css = css.replaceAll('</', '<\\/'); ``` This protects the style-element boundary. The vulnerable path above produces `inline` output instead of `css`, so it never reaches that replacement. ## Affected Configurations and Versions The verified configuration has all of the following properties: - A widget defines a style field with a free-form type such as `string`. - The field uses `property` without a selector, causing inline-style rendering, or uses `class: true` with an unrestricted string value. - The attacker has permission to edit a widget containing the field. - The malicious content is published, or a higher-privileged user views the affected draft. Standard range, select, and color presets do not directly provide an unrestricted quote-containing value and were not demonstrated as affected by this variant. ## Proof of Concept ### Step 1: Configure a widget with a free-form inline style Add a style field similar to the following to a widget module: ```javascript module.exports = { extend: '@apostrophecms/widget-type', options: { label: 'Style Attribute Test' }, styles: { add: { customFont: { type: 'string', label: 'Custom Font', property: 'font-family' } } } }; ``` The absence of `selector` makes the generated declaration inline. ### Step 2: Store the payload As a user who may edit this widget, enter the following value in `customFont` and save the widget: ```text serif" tabindex="0" autofocus onfocus="document.body.dataset.inline='executed' ``` Publish the page, or have another user view the affected draft. ### Step 3: Reproduce the renderer output directly The vulnerable data flow can also be reproduced using the real renderer and serializer from a checked-out, dependency-installed repository: ```javascript 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); })(); ``` ### Step 4: Observe the unsafe HTML The current renderer produced: ```text inline: font-family: serif" tabindex="0" autofocus onfocus="document.body.dataset.inline='executed'; ``` The serializer then produced an attribute string equivalent to: ```html 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';" ``` The attacker-controlled quote terminates the `style` attribute. `tabindex`, `autofocus`, and `onfocus` become separate live attributes. ### Step 5: Confirm execution in a browser When the generated wrapper was loaded in Chrome 153, autofocus triggered the injected handler automatically. The DOM changed to: ```html <body data-inline="executed"> <div id="audit-inline" tabindex="0" autofocus="" onfocus="..."></div> </body> ``` The safe behavior would be for the quote to appear as `&quot;` inside the `style` attribute, with no injected attributes and no change to `document.body.dataset`. ## Impact - **Stored XSS on published pages:** JavaScript executes for unauthenticated visitors who load a page containing the affected widget. - **Privilege escalation:** If an administrator or another privileged editor views the page or draft, the payload executes with that user's application-origin privileges and can perform authenticated actions available to the victim. - **Persistent content manipulation:** The payload is stored with widget content and continues to execute until the style value is removed or the output path is fixed. - **Phishing and data access:** The script can alter the rendered page, read non-HttpOnly same-origin data, and issue same-origin requests as the victim. ## Recommended Fix ### Fix 1: HTML-escape every string-returning attribute value The string branch of `getWidgetAttributes()` should encode values for a double-quoted HTML attribute context before concatenation. At minimum, encode `&`, `<`, `>`, `"`, and `'` using a centralized Apostrophe HTML encoder. Conceptually: ```javascript if (options.asObject) { return Object.fromEntries(attrs); } return attrs .map(([ key, value ]) => { return `${key}="${escapeHtmlAttribute(String(value))}"`; }) .join(' '); ``` The structured `asObject` branch should remain structured so that its eventual template/DOM renderer can perform context-appropriate encoding. ### Fix 2: Validate style values by their destination grammar Output encoding is the primary fix. As defense in depth: - Require select/range/color or another constrained type for common CSS properties. - Parse and validate free-form CSS property values instead of treating them as arbitrary strings. - For `class: true`, accept only individual valid class-name tokens rather than an unrestricted string.

06

quellen

Welche Quellen diesen Datensatz belegen und wann.

veröffentlicht 2026-10-20geändert 2026-10-20
08

Referenzen (4) nvd

Externe Belege — Patches, Meldungen, Analysen.

scry

Angreifer nutzen Schwachstellen wie diese gegen im Internet exponierte Dienste aus. Sehen Sie mit einem kostenlosen Passiv-Scan, was exponiert ist.

Kostenloser Scan