Reports
MEDIA[XSS]#3727743

Rails ActionText: una etiqueta <action-text-markdown> esquiva la validación de esquemas URI en to_markdown

En ActionText, un texto enriquecido con una etiqueta <action-text-markdown> escrita por el usuario salía tal cual en la exportación a Markdown, lo que permitía colar enlaces javascript: o data: que la validación de URI debía bloquear.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

ActionText, el componente de Rails para contenido enriquecido, puede convertir el cuerpo de un texto a Markdown con Content#to_markdown (y ActionText::RichText#to_markdown). Internamente, al serializar los adjuntos, ese método envuelve el Markdown ya generado en una etiqueta propia, <action-text-markdown>, para que el conversor lo emita sin tocarlo.

El fallo es que el conversor no distingue entre las etiquetas <action-text-markdown> que crea el propio Rails y las que escribe el usuario en el cuerpo del texto. Cualquier <action-text-markdown> se trata como marcado de confianza y su contenido se copia literal a la salida. Así, un usuario que puede redactar texto enriquecido consigue meter en el Markdown exportado un enlace con un esquema peligroso (javascript:, data:…), saltándose la validación de esquemas URI que se había añadido para los enlaces normales y las imágenes remotas.

El código implicado está en actiontext/lib/action_text/markdown_conversion.rb. Primero, la etiqueta figura en la lista de padres cuyo contenido no se escapa (línea 80):

SKIP_ESCAPING_PARENTS = %w[ action-text-markdown code pre ].freeze

Cuando un nodo cae en esa lista, se devuelve su contenido sin procesar (líneas 96-97):

elsif skip_markdown_escaping?(node)
  node.content

Y el visitante de esta etiqueta simplemente une los hijos, sin envolverlos en nada (líneas 226-228):

def visit_action_text_markdown(_node, child_values)
  join_children(child_values)
end

pre y code están en la misma lista, pero sus visitantes meten la salida dentro de un bloque de código, de modo que un enlace ahí dentro queda inerte. Solo <action-text-markdown> lo emite en bruto, y por eso es la vía para saltarse la validación.

Pasos de reproducción

  1. Crear el test actiontext/test/unit/markdown_passthrough_bypass_test.rb con este contenido:
require "test_helper"

class ActionTextMarkdownPassthroughBypassTest < ActiveSupport::TestCase
  test "user-supplied <action-text-markdown> emits markdown link bypassing URI scheme validation" do
    payload = "<action-text-markdown>[click](javascript:alert(1))</action-text-markdown>"
    markdown = ActionText::Content.new(payload).to_markdown

    refute_match(/\(javascript:/, markdown,
      "expected URI scheme validation to reject javascript: URI, got: #{markdown.inspect}")
  end
end
  1. Ejecutarlo desde la raíz del repositorio de Rails:
cd actiontext && bin/test test/unit/markdown_passthrough_bypass_test.rb
  1. El test falla porque el enlace javascript: aparece intacto en el Markdown:
Failure:
expected URI scheme validation to reject javascript: URI, got: "[click](javascript:alert(1))"
  1. Como control, los tests que ya existían para bloquear esquemas no permitidos en imágenes remotas siguen pasando, lo que confirma que la protección existe pero esta etiqueta la esquiva:
cd actiontext && bin/test test/unit/markdown_conversion_test.rb -n '/RemoteImage attachment with disallowed URI scheme|RichText#to_markdown/'
# 5 runs, 5 assertions, 0 failures, 0 errors

Impacto

Cualquier sistema que consuma la salida de ActionText::RichText#to_markdown o Content#to_markdown (por ejemplo, un panel de vista previa, un resumen por correo, una réplica federada o un segundo renderizador de Markdown) puede recibir un enlace Markdown con esquema javascript:, data: u otro no seguro, controlado por cualquier usuario que pueda escribir texto enriquecido.

La barrera de seguridad sobre URIs que introdujeron los PR #56871 y #56909 para los enlaces <a> y los adjuntos de imagen remota queda anulada en la exportación a Markdown con solo envolver el payload en <action-text-markdown>.

El renderizado HTML (Content#to_s) no se ve afectado directamente: ahí la etiqueta se elimina y el payload se muestra como texto inerte. El problema es exclusivo de la exportación a Markdown.

HackerOne clasificó el reporte con severidad media. Se envió el 11 de mayo de 2026, figura como resuelto y se divulgó el 24 de agosto de 2026.

Remediación

El investigador propuso eliminar las etiquetas <action-text-markdown> que vengan del usuario (conservando su contenido como texto normal) antes de que to_markdown genere sus propios marcadores internos:

--- a/actiontext/lib/action_text/content.rb
+++ b/actiontext/lib/action_text/content.rb
@@ -146,6 +146,11 @@ module ActionText
       end

       def to_markdown(attachment_links: false)
+        # Prevent user-supplied <action-text-markdown> nodes from being treated
+        # as trusted internal markdown passthrough by the markdown converter.
+        fragment.find_all("action-text-markdown").each do |node|
+          node.replace(node.children)
+        end
         render_attachments(with_full_attributes: false) { |attachment|
           ActionText::HtmlConversion.create_element("action-text-markdown").tap do |node|
             node.content = attachment.to_markdown(attachment_links: attachment_links)

fragment.find_all es el mismo recurso de Nokogiri que ya se usa en otras partes de content.rb (líneas 56 y 210). Según el investigador, con este cambio el test de la prueba de concepto pasa y los tests existentes de Markdown para adjuntos no se ven afectados. El reporte consta como resuelto, pero no detalla qué corrección aplicó finalmente el equipo de Rails.