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:
- User Input: A user clicks "Insert video" or "Insert post" in the Tiptap editor and enters a URL into the
EmbedMediaForm modal.
- 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).
- 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).
- 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).
- 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).
- 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).
- 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).
- 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.)
- Navigate to a page using the rich text editor (e.g., creating/editing a product description or post).
- Click the "Insert video" or "Insert post" button in the editor toolbar.
- 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.
- Click "Insert".
- 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:
- 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.
- 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.
- 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.
Component:
/app/app/javascript/components/TiptapExtensions/MediaEmbed.tsxVulnerability: Potential DOM-based Cross-Site Scripting (XSS) via unsanitized HTML from
iframe.ly.Source: User-controlled URL input provided to the media embed dialog (
EmbedMediaFormcomponent, triggered from the Tiptap editor menu).Data Flow:
EmbedMediaFormmodal.iframe.lyservice API:https://iframe.ly/api/oembed?iframe=1&api_key=...&url=<encoded_user_url>(SeeMediaEmbed.tsx:146).iframe.ly. This response is expected to contain anhtmlproperty (data.html) which holds the embeddable HTML code generated byiframe.lybased on the user's URL (SeeMediaEmbed.tsx:148-149).insertMediaEmbedhelper function (MediaEmbed.tsx:185) receives thisdataobject containingdata.html.MEDIA_EMBED_SUPPORTING_PROVIDERS), it attempts to parsedata.htmland extract only theouterHTMLof an<iframe>tag (MediaEmbed.tsx:187-189).<iframe>is found withindata.htmlfor a supported provider, it falls back to using the originaldata.html.data.htmlwhen calling thesetRawcommand (MediaEmbed.tsx:195).htmlstring is passed as an attribute to either theinsertMediaEmbedTiptap command (MediaEmbed.tsx:190) or thesetRawcommand (MediaEmbed.tsx:195).mediaEmbedorrawnode and storing the received HTML string in the node's attributes (node.attrs.htmlorHTMLAttributes.html).dangerouslySetInnerHTML): TheExternalMediaFileEmbedReact component renders themediaEmbednode. It usesdangerouslySetInnerHTMLto render the preview, directly injecting the stored HTML:<div className="preview" dangerouslySetInnerHTML={{ __html: cast(node.attrs.html) }}></div>(MediaEmbed.tsx:246).innerHTML): TheRawnode renderer (used ifsetRawwas called) also uses aninnerHTMLsink:doc.innerHTML = cast(HTMLAttributes.html);(MediaEmbed.tsx:67).Risk:
The application relies on the external
iframe.lyservice to provide safe, embeddable HTML. If an attacker can provide a crafted URL that causesiframe.lyto return HTML containing malicious code (e.g.,<script>,onerrorattributes, etc.), and this HTML bypasses the simpleiframeextraction logic (or uses thesetRawpath), it will be stored and subsequently rendered directly into the DOM viadangerouslySetInnerHTMLorinnerHTML. 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.lyreturn malicious HTML for a given URL. This might require exploitingiframe.lyitself or finding specific edge cases it handles insecurely.)iframe.ly. Examples (purely illustrative, likely blocked byiframe.ly):data:URL:data:text/html,<svg onload=alert(document.domain)>iframe.lymight misinterpret.iframe.lyis known (or discovered) to return unsanitized script content.iframe.lyinto returning malicious HTML, and that HTML reaches thedangerouslySetInnerHTMLorinnerHTMLsink, the script should execute.Recommendation:
dangerouslySetInnerHTMLorinnerHTML, especially when the input driving the external service is user-controlled.iframe.lyand before passing it to the sink. Use a well-vetted sanitization library (like DOMPurify) configured appropriately.<iframe>(<iframe sandbox>) to isolate it from the main application context, significantly reducing the impact of potential XSS within the embed.