Skip to content

Stored XSS via Unsanitized Product Description #3

@ghost

Description

Vulnerability: Stored XSS via Product Description

Location: Product page rendering (/app/app/javascript/components/Product/index.tsx), originating from product update logic (/app/app/controllers/links_controller.rb and /app/app/services/save_public_files_service.rb).

Description:
When a seller updates a product, the description content is processed by SavePublicFilesService. This service uses Nokogiri::HTML.fragment to parse the description and primarily focuses on validating and cleaning <public-file-embed> tags.

# /app/app/services/save_public_files_service.rb
def process
  ActiveRecord::Base.transaction do
    # ... (logic for handling public-file-embed)
    doc = Nokogiri::HTML.fragment(content)
    # ...
    clean_invalid_file_embeds(doc, persisted_files)
    doc.to_html # Returns potentially unsanitized HTML
  end
end

The service does not appear to perform general HTML sanitization on the description content (content). It returns the processed HTML (doc.to_html), which is then saved to the product.description field in LinksController#update:

# /app/app/controllers/links_controller.rb
@product.description = SavePublicFilesService.new(..., content: @product.description).process
@product.save!

On the product page (/app/app/javascript/components/Product/index.tsx), during the initial render (pageLoaded is false), the component uses dangerouslySetInnerHTML to render product.description_html:

// /app/app/javascript/components/Product/index.tsx
{
  pageLoaded ? (
    <EditorContent className="rich-text" editor={descriptionEditor} />
  ) : (
    <div className="rich-text" dangerouslySetInnerHTML={{ __html: product.description_html ?? "" }} />
  )
}

If product.description_html is derived directly from the potentially unsanitized product.description saved by the backend (without an intermediate sanitization step during HTML generation), then any malicious HTML/JavaScript saved in the description by the seller will be rendered and executed in the browser of users visiting the product page.

Source: Product description field controlled by the seller.
Sink: dangerouslySetInnerHTML in /app/app/javascript/components/Product/index.tsx.
Sanitization: Incomplete; SavePublicFilesService doesn't sanitize general HTML. Vulnerability depends on whether description_html is generated safely before being passed to the frontend, but the direct sink usage suggests a potential gap.

Reproduction / Exploitation:

  1. As a seller, create or edit a product.
  2. In the description field, insert an XSS payload, e.g., <img src=x onerror=alert('XSS_in_Product_Description')>.
  3. Save the product.
  4. Visit the public page for that product as a different user (or logged out).
  5. If the vulnerability exists, the JavaScript payload should execute when the description is rendered.

Remediation:

  1. Backend Sanitization: Ensure that the product.description field is sanitized before saving using a robust HTML sanitizer (like rails-html-sanitizer) within the LinksController#update action or SavePublicFilesService. Remove any dangerous tags (<script>, <iframe>, etc.) and attributes (onerror, onload, etc.).
  2. Frontend Sanitization (Less Ideal): If backend sanitization is not feasible, ensure that the product.description_html prop passed to the frontend component is always generated through a safe process (e.g., Markdown rendering with sanitization enabled) before being sent in the initial page data. Avoid directly reflecting the raw saved description content.
  3. Avoid dangerouslySetInnerHTML: If possible, refactor the initial render to use the Tiptap EditorContent like the pageLoaded state, or render the description as plain text if HTML is not strictly required before the editor loads.

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