Teable: Stored XSS in Notifications via the Display Name

A self-registered account runs script code through its display name in the session of every user it invites into a base.

Advisory ID: TP-2026-111
Product: Teable (no-code database and spreadsheet application)
Vulnerability type: Stored cross-site scripting (CWE-79)
CVE: pending
CVSS 3.1: 4.6 (Medium) · CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:N
Affected versions: < release.2026-10-12T12-45-53Z.3331 (tested with 1.10.0)
Vendor advisory: GHSA-3rmq-5wwc-x8w2
Fixed in: release.2026-10-12T12-45-53Z.3331
Reported: 16 July 2026

Summary

Teable is a no-code database with a spreadsheet interface in which users invite each other into bases and assign each other in records. The in-app notifications inserted their text into the page as HTML via dangerouslySetInnerHTML, and that text contained the triggering user's display name without any escaping. A self-registered user sets their display name to HTML with an event handler, invites the victim into their own base and assigns the victim in a user field. When the victim opens the notification panel, the attacker's script runs in the victim's authenticated context and can create API tokens, change the email address or pull data on the victim's behalf.

Root cause

The notification component renders the message with <div dangerouslySetInnerHTML={{ __html: message }}> in both branches (apps/nextjs-app/src/features/app/components/notifications/notification-component/LinkNotification.tsx:46,60), and so does the popup for important notices (apps/nextjs-app/src/features/app/components/notifications/ImportantNotificationPopup.tsx:58). The message is built in getShowMessage (apps/nextjs-app/src/features/app/components/notifications/notification-component/get-show-message.ts:6) from the template collaboratorCellTag.subject, which inserts {{fromUserName}}, {{fieldName}} and {{tableName}} without HTML escaping (packages/common-i18n/src/locales/en/common.json:1212). The display name is stored via PATCH /api/user/name as z.string().min(1).max(100) with no filtering (packages/openapi/src/user/update-name.ts:11). CollaboratorNotificationListener copies the sender name, field name and table name verbatim into the victim's notification (collaborator-notification.listener.ts:98-110). Self-registration is open by default and the frontend CSP has no script-src directive, so the inline handler executes.

Proof of Concept

PATCH /api/user/name
Cookie: <session of a self-registered account>

{"name": "<img src=x onerror=alert(document.domain)>"}

Attacker: create space, base and table, invite the victim,
          add a user field, assign the victim in a cell
Victim:   open the app -> click the bell -> alert(document.domain)

The assignment creates a notification for the victim whose text contains the raw display name. When the notification panel opens, React inserts the text as HTML and the onerror handler runs in the origin of the Teable instance.

Impact

  • Execution of attacker JavaScript in the authenticated session of every user the attacker reaches through an invitation.
  • Creating API tokens and changing the email address on the victim's behalf, up to account takeover.
  • Reading and exfiltrating every base and table the victim can access.
  • Repeated execution each time the notifications are opened, for as long as the notification exists.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.