Helpy 2.8.0 - Stored XSS in knowledgebase Doc body rendering

4,8

Medium

Detected by

Fluid Attacks AI SAST Scanner

Disclosed by

Oscar Uribe

Summary

Full name

Helpy 2.8.0 - Stored XSS in knowledgebase Doc body via DocsHelper#sanitize_doc_content allowing JavaScript execution in any visitor session

Code name

State

Public

Release date

Affected product

helpy

Vendor

helpy.io

Affected version(s)

2.8.0

Vulnerability name

Stored cross-site scripting (XSS)

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N

CVSS v4.0 base score

4.8

Exploit available

Yes

Description

Helpy 2.8.0 contains a stored cross-site scripting vulnerability in the knowledgebase Doc rendering logic. An authenticated attacker with admin or agent editor privileges can persist arbitrary HTML or JavaScript in the body field of a knowledgebase Doc and cause it to execute in the browser of any user who views the rendered article, including unauthenticated visitors.

The vulnerability exists in DocsHelper#sanitize_doc_content, which marks rendered Doc content as trusted with .html_safe without performing sanitization. The affected data flow is: attacker-controlled body input is stored in the database, converted to HTML by RDiscount, which preserves raw HTML tags, then passed to sanitize_doc_content, marked .html_safe, and emitted through ERB templates with <%= ... %>. Because Rails auto-escaping is bypassed, malicious HTML or JavaScript stored in the Doc body is rendered verbatim.

Vulnerability

Source → sink path

  1. Source persistence: attacker-controlled body parameter accepted by:

  • Admin::DocsController#create and #update (web form, requires verify_editor before_action)

  • API::V1::Docs POST /api/v1/docs and PATCH /api/v1/docs/:id (requires admin or agent role via X-Token header)

  • No input sanitization is performed before Doc.save / Doc.update_attributes.

  1. Storage: Doc#body (PostgreSQL text column) stores the raw payload.

  2. Markdown-to-HTML conversion (app/models/doc.rb:89-92):

def content
  c = RDiscount.new(self.body)
  c.to_html
end
def content
  c = RDiscount.new(self.body)
  c.to_html
end
def content
  c = RDiscount.new(self.body)
  c.to_html
end
def content
  c = RDiscount.new(self.body)
  c.to_html
end

RDiscount passes raw HTML tags in the Markdown source through to the output unchanged. A <script> block in body survives to_html verbatim.

  1. Misleadingly-named sink (app/helpers/docs_helper.rb:29-31):

def sanitize_doc_content(content)
  "#{content}".html_safe
end
def sanitize_doc_content(content)
  "#{content}".html_safe
end
def sanitize_doc_content(content)
  "#{content}".html_safe
end
def sanitize_doc_content(content)
  "#{content}".html_safe
end

.html_safe instructs Rails ERB not to HTML-escape the string. No sanitization whatsoever is performed despite the method name.

  1. Render in public and admin views — four templates call <%= sanitize_doc_content(@doc.content) %>:

  • app/views/docs/show.html.erb:50public, unauthenticated access

  • app/views/admin/internal_docs/show.html.erb:39 — internal admin view

  • app/themes/nordic/views/docs/show.html.erb:56 — Nordic theme (public)

  • app/themes/singular/views/docs/show.html.erb:34 — Singular theme (public)

Relevant code:

  • app/helpers/docs_helper.rb:29-31

  • app/models/doc.rb:89-92

  • app/controllers/admin/docs_controller.rb:24-33 (create) and 35-58 (update)

  • app/controllers/api/v1/docs.rb:46-60 (POST) and 80-95 (PATCH)

  • app/views/docs/show.html.erb:50

  • app/views/admin/internal_docs/show.html.erb:39

  • app/themes/nordic/views/docs/show.html.erb:56

  • app/themes/singular/views/docs/show.html.erb:34

PoC

Reproduction used in the validation environment

Environment: http://localhost:3000 running via docker compose up from the repo root. Seed credentials: admin@test.com / 12345678

  1. Log in as admin at http://localhost:3000/en/users/sign_in using admin@test.com / 12345678.

  2. Navigate to http://localhost:3000/admin/docs/new?lang=en.

    The ?lang=en parameter is required — without it the category dropdown is not rendered and the form submits with no category assigned.

  3. Fill in the form:

    • Title: Stored XSS PoC

    • Category: Getting Started

    • Body: click the </> (Code View) button in the Summernote toolbar and paste the payload directly into the HTML editor:

      <script>alert(document.cookie)</script>
      <script>alert(document.cookie)</script>
      <script>alert(document.cookie)</script>
      <script>alert(document.cookie)</script>
    • Status: Published

  4. Click Save Changes. The application stores the raw <script> tag verbatim in docs.body with no sanitization.

  5. Note the doc ID from the redirect URL (e.g. /admin/docs/7/edit) and open the public article as an unauthenticated visitor (incognito window):

    http://localhost:3000/en/docs/7-stored-xss-poc
    http://localhost:3000/en/docs/7-stored-xss-poc
    http://localhost:3000/en/docs/7-stored-xss-poc
    http://localhost:3000/en/docs/7-stored-xss-poc

    The alert(document.cookie) dialog fires immediately on page load, confirming JavaScript execution in the visitor's browser context.

Evidence of Exploitation

  • Video of Exploitation

  • Static Evidence as an anonymous user visiting the document.

Our security policy

We have reserved the ID CVE-2026-40230 to refer to this issue from now on.

Disclosure policy

System Information

  • Helpy

  • Version 2.8.0

  • Operating System: Any

References

Github Repository: https://github.com/helpyio/helpy
Security: https://github.com/helpyio/helpy/security

Mitigation

There is currently no patch available for this vulnerability.

Credits

The vulnerability was discovered by Oscar Uribe from Fluid Attacks' Offensive Team using the AI SAST Scanner.

Timeline

Vulnerability discovered

Vendor contacted

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3