Skip to content

Potential DOM XSS in Media Embed Feature via iframe.ly Response #2

@ghost

Description

Component: /app/app/javascript/components/TiptapExtensions/MediaEmbed.tsx

Vulnerability: Potential DOM-based Cross-Site Scripting (XSS) via unsanitized HTML from iframe.ly.

Source: User-controlled URL input provided to the media embed dialog (EmbedMediaForm component, triggered from the Tiptap editor menu).

Data Flow:

  1. User Input: A user clicks "Insert video" or "Insert post" in the Tiptap editor and enters a URL into the EmbedMediaForm modal.
  2. External API Call: The frontend takes the user's URL, encodes it, and sends it to the external iframe.ly service API: https://iframe.ly/api/oembed?iframe=1&api_key=...&url=<encoded_user_url> (See MediaEmbed.tsx:146).
  3. HTML Received: The application receives a JSON response from iframe.ly. This response is expected to contain an html property (data.html) which holds the embeddable HTML code generated by iframe.ly based on the user's URL (See MediaEmbed.tsx:148-149).
  4. Internal Processing: The insertMediaEmbed helper function (MediaEmbed.tsx:185) receives this data object containing data.html.
    • For certain supported providers (MEDIA_EMBED_SUPPORTING_PROVIDERS), it attempts to parse data.html and extract only the outerHTML of an <iframe> tag (MediaEmbed.tsx:187-189).
    • Fallback: If no <iframe> is found within data.html for a supported provider, it falls back to using the original data.html.
    • Unsupported Provider: If the provider is not in the supported list, it directly uses the raw data.html when calling the setRaw command (MediaEmbed.tsx:195).
  5. Tiptap Command: The processed (or raw) html string is passed as an attribute to either the insertMediaEmbed Tiptap command (MediaEmbed.tsx:190) or the setRaw command (MediaEmbed.tsx:195).
  6. Node Attribute Update: These commands update the Tiptap editor's state, creating/updating a mediaEmbed or raw node and storing the received HTML string in the node's attributes (node.attrs.html or HTMLAttributes.html).
  7. Sink 1 (dangerouslySetInnerHTML): The ExternalMediaFileEmbed React component renders the mediaEmbed node. It uses dangerouslySetInnerHTML to render the preview, directly injecting the stored HTML: <div className="preview" dangerouslySetInnerHTML={{ __html: cast(node.attrs.html) }}></div> (MediaEmbed.tsx:246).
  8. Sink 2 (innerHTML): The Raw node renderer (used if setRaw was called) also uses an innerHTML sink: doc.innerHTML = cast(HTMLAttributes.html); (MediaEmbed.tsx:67).

Risk:

The application relies on the external iframe.ly service to provide safe, embeddable HTML. If an attacker can provide a crafted URL that causes iframe.ly to return HTML containing malicious code (e.g., <script>, onerror attributes, etc.), and this HTML bypasses the simple iframe extraction logic (or uses the setRaw path), it will be stored and subsequently rendered directly into the DOM via dangerouslySetInnerHTML or innerHTML. This executes the malicious script in the context of the user's session.

Hypothetical Reproduction Steps:

(Note: Successful exploitation depends on finding a way to make iframe.ly return malicious HTML for a given URL. This might require exploiting iframe.ly itself or finding specific edge cases it handles insecurely.)

  1. Navigate to a page using the rich text editor (e.g., creating/editing a product description or post).
  2. Click the "Insert video" or "Insert post" button in the editor toolbar.
  3. In the URL input field, enter a URL crafted to exploit iframe.ly. Examples (purely illustrative, likely blocked by iframe.ly):
    • A data: URL: data:text/html,<svg onload=alert(document.domain)>
    • A URL pointing to a page with malicious OpenGraph/oEmbed tags that iframe.ly might misinterpret.
    • Any URL for which iframe.ly is known (or discovered) to return unsanitized script content.
  4. Click "Insert".
  5. If the crafted URL successfully tricks iframe.ly into returning malicious HTML, and that HTML reaches the dangerouslySetInnerHTML or innerHTML sink, the script should execute.

Recommendation:

  1. Avoid Direct Rendering: Do not directly render HTML received from external services using dangerouslySetInnerHTML or innerHTML, especially when the input driving the external service is user-controlled.
  2. Sanitization: If rendering external HTML is unavoidable, rigorously sanitize it after receiving it from iframe.ly and before passing it to the sink. Use a well-vetted sanitization library (like DOMPurify) configured appropriately.
  3. Sandboxing: Consider rendering the embed within a sandboxed <iframe> (<iframe sandbox>) to isolate it from the main application context, significantly reducing the impact of potential XSS within the embed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions