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:
- As a seller, create or edit a product.
- In the description field, insert an XSS payload, e.g.,
<img src=x onerror=alert('XSS_in_Product_Description')>.
- Save the product.
- Visit the public page for that product as a different user (or logged out).
- If the vulnerability exists, the JavaScript payload should execute when the description is rendered.
Remediation:
- 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.).
- 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.
- 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.
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.rband/app/app/services/save_public_files_service.rb).Description:
When a seller updates a product, the description content is processed by
SavePublicFilesService. This service usesNokogiri::HTML.fragmentto parse the description and primarily focuses on validating and cleaning<public-file-embed>tags.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 theproduct.descriptionfield inLinksController#update:On the product page (
/app/app/javascript/components/Product/index.tsx), during the initial render (pageLoadedis false), the component usesdangerouslySetInnerHTMLto renderproduct.description_html:If
product.description_htmlis derived directly from the potentially unsanitizedproduct.descriptionsaved 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:
dangerouslySetInnerHTMLin/app/app/javascript/components/Product/index.tsx.Sanitization: Incomplete;
SavePublicFilesServicedoesn't sanitize general HTML. Vulnerability depends on whetherdescription_htmlis generated safely before being passed to the frontend, but the direct sink usage suggests a potential gap.Reproduction / Exploitation:
<img src=x onerror=alert('XSS_in_Product_Description')>.Remediation:
product.descriptionfield is sanitized before saving using a robust HTML sanitizer (likerails-html-sanitizer) within theLinksController#updateaction orSavePublicFilesService. Remove any dangerous tags (<script>,<iframe>, etc.) and attributes (onerror,onload, etc.).product.description_htmlprop 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 saveddescriptioncontent.dangerouslySetInnerHTML: If possible, refactor the initial render to use the TiptapEditorContentlike thepageLoadedstate, or render the description as plain text if HTML is not strictly required before the editor loads.