Ir al contenido
Acecore
Índice
Renderizar con seguridad enlaces Markdown en respuestas de chat con IA

Si un chat con IA responde Consulta [Servicios]( /services/ ), el enlace puede no renderizarse y el Markdown bruto puede quedar visible.

Acecore encontró este caso en el chat de contacto y ajustó el renderizador en el PR que corrigió el renderizado de enlaces Markdown.

Este artículo toma esa corrección pequeña como punto de partida para convertir respuestas de IA en DOM de forma segura.

Las respuestas de IA no son HTML confiable

La salida del modelo debe tratarse como texto.

En un chat son útiles los enlaces, negritas y listas, pero poner la respuesta en innerHTML permite que el navegador interprete cualquier cadena generada por el modelo.

No necesitas implementar Markdown completo. Necesitas detectar solo las expresiones que el chat soporta y crear nodos DOM seguros.

El problema no es solo el espacio

El bug concreto era un enlace como:

[Servicios](/services/)

Una expresión regular estricta suele asumir que la URL no contiene espacios:

;/\[([^\]]+)\]\(([^)\s]+)\)/

[^)\s]+ rechaza espacios, por lo que ( /services/ ) no se reconoce. La solución es tolerar espacios dentro del paréntesis y normalizar después.

;/\[([^\]]+)\]\(\s*([^)]+?)\s*\)/

Pero relajar el parser no basta. Después hay que validar el valor normalizado.

Recorta href antes de validar

El orden debe ser estable:

  1. Extraer label y raw href
  2. Ejecutar trim() sobre raw href
  3. Validar el href recortado con una allowlist
  4. Crear <a> solo si está permitido
const href = String(rawHref || '').trim()

if (label && isSafeMarkdownHref(href)) {
  const link = document.createElement('a')
  link.href = href
  link.rel = 'noopener noreferrer'

  if (/^https?:\/\//i.test(href)) {
    link.target = '_blank'
  }

  link.textContent = label
  parent.appendChild(link)
}

Valida exactamente el mismo valor que insertas en el DOM. Si no, el control pierde fuerza.

La allowlist depende del producto

Cada sitio decide qué URLs puede mostrar su IA.

TipoEjemploDecisión
Ruta interna/services/Permitir
Mismo originhttps://acecore.net/...Permitir
LINE oficialhttps://lin.ee/...Permitir si es el canal oficial
mailtomailto:[email protected]Solo dirección fija
teltel:05088902788Solo número fijo
Otros externosCualquier URLNo enlazar por defecto
function isSafeMarkdownHref(href) {
  if (href.startsWith('/')) return true

  try {
    const url = new URL(href, window.location.origin)
    if (url.origin === window.location.origin) return true
    if (url.hostname === 'acecore.net') return true
    if (url.hostname === 'lin.ee') return true
  } catch {
    return false
  }

  return href === 'mailto:[email protected]' || href === 'tel:05088902788'
}

Un sitio de empleo puede permitir portales de reclutamiento; un SaaS puede permitir documentación y página de estado. La función debe ser propia de cada producto.

Fallback a texto para enlaces no permitidos

Cuando un enlace no pasa la validación, no siempre conviene borrarlo.

En un chat de contacto, dejar el Markdown como texto conserva contexto para el usuario y ayuda a depurar qué intentó emitir el modelo.

El renderizador debe saber crear enlaces seguros y también fallar de forma segura.

Pruebas mínimas

Prueba más que el caso feliz.

EntradaResultado esperado
[Servicios](/services/)Enlace interno
[Servicios]( /services/ )Enlace interno después de trim
[LINE]( https://lin.ee/example )Enlace externo permitido
[Malo](javascript:alert(1))No se enlaza
[Externo](https://example.com/)No se enlaza si el dominio no está permitido
[Roto](/services/Se muestra como texto

En PR #99 se confirmó que las variantes con y sin espacios producen la misma URL esperada.

No implementes todo Markdown por defecto

Para un chat suele bastar con:

  • Párrafos
  • Listas
  • Negrita
  • Código inline
  • Enlaces

Tablas, imágenes, HTML crudo y notas al pie amplían demasiado la responsabilidad. Si usas una librería más adelante, la política de HTML y URLs sigue siendo una decisión separada.

Resumen

Renderizar enlaces Markdown en respuestas de IA parece un detalle de UI, pero define cuánto confías en la salida del modelo.

La pauta es simple: texto primero, subconjunto pequeño, trim antes de validar, allowlist estricta y fallback seguro.

Flujo de renderizado de enlaces

Text

Tratar la respuesta del modelo como texto plano.

Parse

Detectar solo el Markdown que el chat realmente soporta.

Validate

Recortar href y permitir solo URLs internas o dominios aprobados.

Render

Crear elementos seguros con DOM API, no con innerHTML.

Decisiones que conviene separar

Renderizado laxo

  • Poner respuestas de IA directamente en innerHTML
  • Intentar implementar todo Markdown de una vez
  • Fallar cuando la URL tiene espacios alrededor
  • Tratar URLs externas y javascript: como si fueran iguales

Renderizado pequeño y seguro

  • Recibir respuestas como texto y convertir solo lo necesario en DOM
  • Soportar solo el subconjunto usado por el chat
  • Validar URLs después de trim
  • Mantener URLs no permitidas como texto
Checklist de implementación
  • Completado: No confiar en respuestas de IA como HTML
  • Completado: Aceptar espacios alrededor de URLs Markdown
  • Completado: Hacer trim de href antes de validar
  • Completado: Permitir solo rutas internas, origin actual y dominios necesarios
  • Completado: Definir target y rel en enlaces externos
  • Completado: Conservar enlaces no permitidos como texto
  • Completado: Probar URLs peligrosas y Markdown roto
Preguntas frecuentes
¿Basta con usar markdown-it o marked?
Incluso usando una librería, hay que decidir cómo tratar HTML, qué destinos de enlace permitir, cómo añadir target y rel, y cómo rechazar URLs peligrosas. Para chat, un renderizador pequeño puede ser suficiente.
¿Permitir espacios alrededor de la URL es peligroso?
El riesgo no está en los espacios. Lo importante es validar el href después de trim. Así toleras variaciones del modelo sin relajar la allowlist.
¿Hay que eliminar las URLs no permitidas?
Normalmente dejarlas como texto facilita depurar y conserva contexto. Si la política exige ocultarlas, también puedes eliminar el enlace completo.

Comentarios

Cargando comentarios...

No se pueden publicar enlaces, correos ni textos promocionales.

G

Gui

CEO de Acecore. Lidera sistemas de negocio, web, bases de datos e infraestructura, calidad y adopción de IA desde la definición de problemas de negocio hasta el diseño, la puesta en marcha y la mejora posterior. Se apoya en capacidad práctica con C#/.NET y también cubre PHP/JavaScript, SQL Server/PostgreSQL/MySQL y Linux/Windows Server, diseñando requisitos, selección tecnológica, estándares de calidad y operaciones de desarrollo basadas en GitHub como un flujo coherente. Incorpora la IA generativa en procesos de desarrollo, verificación y organización de información, como una base práctica para que equipos pequeños entreguen más rápido y con mayor fiabilidad.

Definición de problemas de negocioSelección tecnológicaDiseño de sistemasC#/.NETDiseño de bases de datos/infraestructuraOperaciones de desarrollo en GitHubIA generativaDiseño de flujos de IADiseño de calidadIntegración en sitio

¿Quiere saber más sobre nuestros servicios?

Ofrecemos soporte integral en desarrollo de sistemas, diseño web, diseño gráfico y educación IT.

Artículos relacionados

Buscar artículos