GHSA-cp6q-959q-f8rh: XSS

Published Sep 2, 2026
·
Updated

Summary

@tiptap/core's public mergeAttributes() helper uses ordinary bracket assignment on keys returned by Object.entries(). An own proto key from JSON therefore invokes the legacy prototype setter on the fresh merged object. The function returns an object whose prototype is attacker-controlled, while Object.keys() and ordinary own-property checks show no attacker attributes.

When that result is used as a ProseMirror DOMOutputSpec attribute object, prosemirror-model's DOMSerializer.renderSpec() enumerates it with for...in and applies inherited values with setAttribute(). In a browser proof, inherited src and onerror values were copied to an <img> and the error handler executed once. This is per-object prototype manipulation; the proof does not modify global Object.prototype.

Root cause

The affected loop is conceptually:

ts const mergedAttributes = { ...items } for (const [key, value] of Object.entries(item)) { const exists = mergedAttributes[key] // ... mergedAttributes[key] = value }

Object.entries(JSON.parse('{"proto": {...}}')) includes proto. Reading mergedAttributes['proto'] resolves the inherited Object.prototype; assigning to the same key invokes Object.prototype.proto's setter and replaces mergedAttributes' prototype.

Browser reproduction

The following shape was tested with exact @tiptap/core 3.29.2 and prosemirror-model 1.25.11:

js const input = JSON.parse({ "proto": { "data-inherited-canary": "present", "src": "x-invalid://canary", "onerror": "globalThis.tiptapXss += 1" } })

const attrs = mergeAttributes(input) // Object.keys(attrs) === [] // Object.getPrototypeOf(attrs) === input.proto

const schema = new Schema({ nodes: { doc: { content: 'image' }, image: { toDOM: () => ['img', attrs] }, text: {}, }, }) const doc = schema.node('doc', null, [schema.node('image')]) const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content) document.body.append(fragment)

Chromium produced an image with data-inherited-canary, src, and onerror; the handler executed exactly once. Object.prototype remained clean.

Impact and preconditions

Applications that merge untrusted imported document, plugin, CMS, API, tenant, or AI-derived attribute objects can receive a prototype-manipulated result. Consumers that enumerate inherited keys, including ProseMirror's DOM serializer, can turn the hidden properties into DOM attributes and execute JavaScript in the application's origin. Own-key validation, object spread, JSON serialization, and logging can miss the inherited values. Other component consumers can read inherited authorization or configuration fields.

Tiptap's standard fixed ProseMirror schemas discard unknown document attributes, so arbitrary Tiptap JSON is not automatically exploitable in every application. A vulnerable application needs an untrusted object boundary into mergeAttributes() or a dynamic/custom extension or schema that preserves the relevant attribute object.

Affected versions

The unsafe assignment was introduced in commit ecadf7ea0a7f8f39a8496a60edf0ac8f379e6eb3 and is present in the first package tag @tiptap/core@2.0.0-alpha.0, v2.0.0, v2.27.1, v3.0.0, and current v3.29.2 source. No fixed release was found.

Recommended remediation

Reject proto before reading or assigning the key, or define copied keys as own data properties without invoking legacy setters. A minimal hardening is to skip key === 'proto'. Add regression tests using an own JSON-origin proto key and assert that the result keeps Object.prototype as its prototype, exposes no inherited attacker keys, and cannot create an event-handler attribute through DOMSerializer.

This was found during authorized dependency review and is being reported privately. No public zero-day issue has been opened.

Affected Software

1 affected componentFixes available
npm/@tiptap/core>=2.0.0-alpha.0<3.30.4
3.30.4

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@tiptap/core to a version that resolves this vulnerability.

    Fixed in 3.30.4
  2. Configuration

    Before reading or assigning any attribute keys in the object returned/used by mergeAttributes(), reject the key literally equal to '__proto__' (skip processing for key === '__proto__') to prevent per-object prototype manipulation that can lead to DOMSerializer setting inherited attributes (e.g., src/onerror).

    Application code using @tiptap/core mergeAttributes() / DOMSerializer __proto__ key handling = reject
  3. Operational

    Add regression tests that supply an own JSON-origin '__proto__' key (e.g., via JSON.parse) into the attribute input that reaches mergeAttributes(), then assert: (1) the merged result keeps Object.prototype as its prototype, (2) no inherited attacker keys are exposed/enumerable, and (3) the result cannot create an event-handler attribute through DOMSerializer rendering.

Event History

Sep 2, 2026
Advisory Published
via GitHub·02:44 PM
Data Sourced
via GitHub·02:44 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

What conditions are required for exploitation?

An attacker needs to supply an attribute object with an own __proto__ property, such as one produced by parsing attacker-controlled JSON, that reaches mergeAttributes(). The resulting merged object must then be used as a ProseMirror DOMOutputSpec attribute object and rendered by DOMSerializer.renderSpec() in a browser.

2

Does this modify Object.prototype globally?

No. The described behavior manipulates the prototype of the individual merged object; it does not modify the global Object.prototype.

3

Can ordinary attribute validation or inspection reveal the injected values?

Not reliably. The attacker-controlled values are inherited from the manipulated prototype, so Object.keys() and ordinary own-property checks do not show them, while for...in enumeration can still process them.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203