48. Seguridad {#48-seguridad}
Markdown es texto, pero un pipeline Markdown puede producir HTML, ejecutar JavaScript, cargar recursos remotos o compilar código (MDX). Por eso, un documento Markdown no confiable debe tratarse con las mismas precauciones que cualquier entrada de usuario.
48.1 Modelo de amenaza {#481-modelo-de-amenaza}
flowchart LR
A[Markdown no confiable] --> B[Parser]
B --> C[HTML generado]
C --> D[Sanitizador]
D --> E[DOM del navegador]
C --> F[Recursos remotos]
B --> G[Extensiones y plugins]
G --> H[Código de terceros]
Riesgos principales:
| Riesgo | Descripción |
|---|---|
| XSS | Inyección de scripts que se ejecutan en el navegador de otro usuario |
| HTML injection | Inserción de etiquetas o atributos no deseados |
| CSS injection | Alteración de la interfaz o exfiltración mediante CSS |
| Tracking | Imágenes remotas que revelan IP, hora y agente de usuario |
| SSRF | Si el servidor descarga recursos remotos, puede acceder a redes internas |
| RCE | MDX o plugins que ejecutan código en el servidor |
| DoS | Entradas enormes, expresiones regulares costosas o diagramas complejos |
| Supply chain | Dependencias y plugins comprometidos |
48.2 HTML embebido {#482-html-embebido}
El Markdown original y CommonMark permiten HTML crudo. Eso incluye etiquetas peligrosas:
<script>alert(document.cookie)</script>
<img src="x" onerror="alert('XSS')">
<iframe src="https://ejemplo.com"></iframe>
<a href="javascript:alert(1)">Enlace</a>
Medidas:
- Desactiva el HTML en el parser cuando no sea necesario (
markdown-it({ html: false }),cmarksin--unsafe). - Si necesitas HTML, usa una allowlist de etiquetas y atributos.
- Sanitiza después de generar el HTML, no antes.
- En GitHub, el tagfilter de GFM escapa etiquetas peligrosas y la plataforma sanea el resultado; no asumas que otras plataformas hacen lo mismo.
48.3 URLs y protocolos {#483-urls-y-protocolos}
Los enlaces pueden ejecutar código si el navegador interpreta un esquema peligroso.
| Protocolo | Uso seguro | Notas |
|---|---|---|
https: |
Sí | Preferido |
http: |
Con cautela | Puede mezclar contenido; redirige a HTTPS cuando sea posible |
mailto: |
Sí | Permite abrir el cliente de correo |
tel: |
Sí | En dispositivos compatibles |
javascript: |
No | Ejecuta código |
data: |
No para enlaces | Puede contener HTML/JS; para imágenes, con precaución |
vbscript: |
No | Obsoleto y peligroso |
file: |
No en web | Acceso a archivos locales |
Recomendaciones:
- Valida el esquema con una allowlist antes de renderizar.
- Normaliza la URL y decodifica entidades antes de comprobar (
javascript:puede evadir una comprobación ingenua). - Para enlaces externos con
target="_blank", añaderel="noopener noreferrer". - Muestra el destino real y evita URLs acortadas opacas en contextos de seguridad.
- Ojo con el phishing homográfico (dominios con caracteres Unicode similares).
48.4 JavaScript {#484-javascript}
Markdown no ejecuta JavaScript por sí mismo. El riesgo aparece cuando:
- Se permite HTML con
<script>o atributoson*. - Se usa un enlace
javascript:. - Se inserta HTML generado con
innerHTMLsin sanear. - Se compila MDX, que es código por diseño.
- Un plugin o componente ejecuta código en el servidor.
Si el contenido es de confianza, JavaScript puede aportar interactividad. Si no lo es, no debe ejecutarse.
48.5 XSS {#485-xss}
XSS (Cross-Site Scripting) ocurre cuando una entrada no confiable llega al navegador como código. Tipos:
- Almacenado: el payload se guarda (comentario, perfil, documento) y se ejecuta al visualizarlo.
- Reflejado: el payload viaja en la petición y se devuelve en la respuesta.
- DOM: el código del cliente inserta datos no confiables en el DOM.
Ejemplo de cadena vulnerable:
// ❌ Nunca hagas esto con Markdown de usuarios
const html = md.render(entradaUsuario);
document.getElementById("contenido").innerHTML = html;
Cadena segura:
import DOMPurify from "dompurify";
const html = md.render(entradaUsuario);
const limpio = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["p", "h1", "h2", "h3", "ul", "ol", "li", "a", "code", "pre", "strong", "em", "blockquote"],
ALLOWED_ATTR: ["href", "title", "alt", "src"]
});
document.getElementById("contenido").innerHTML = limpio;
Aun así, la allowlist debe ajustarse al caso. Para enlaces, valida también los protocolos.
- OWASP XSS Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- MDN: tipos de ataques: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks
48.6 Imágenes externas y contenido remoto {#486-imágenes-externas-y-contenido-remoto}
Una imagen remota puede ser un píxel de seguimiento:

Al cargarla, el servidor remoto recibe la IP, el agente de usuario, la hora y el Referer. Medidas:
- Aloja las imágenes en tu propio dominio o en un CDN de confianza.
- Usa un proxy de imágenes que elimine metadatos y cookies.
- Añade
referrerpolicy="no-referrer"yloading="lazy"en HTML. - Restringe
img-srccon una Content Security Policy. - No permitas SVG remotos sin sanear: un SVG puede contener scripts.
- Si el servidor descarga imágenes, valida el destino para evitar SSRF.
48.7 Sanitización {#487-sanitización}
La sanitización elimina etiquetas, atributos y protocolos no permitidos. Debe hacerse con una biblioteca probada, no con expresiones regulares caseras.
| Ecosistema | Biblioteca recomendada |
|---|---|
| JavaScript (navegador) | DOMPurify |
| JavaScript (AST) | rehype-sanitize |
| Python | Bleach (mantenimiento limitado; revisa alternativas) |
| Java | OWASP Java HTML Sanitizer |
| Ruby | Loofah |
| PHP | HTML Purifier |
Ejemplo con rehype-sanitize:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeSanitize from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeSanitize)
.use(rehypeStringify)
.process(markdown);
La sanitización debe aplicarse después de convertir Markdown a HTML y antes de insertarlo en el DOM. Si el HTML se genera en el servidor, sanitiza también en el servidor; si se genera en el cliente, no confíes solo en el cliente.
48.8 MDX y contenido ejecutable {#488-mdx-y-contenido-ejecutable}
MDX compila a JavaScript. Un archivo MDX puede importar módulos, ejecutar expresiones y renderizar componentes. Por tanto:
- No compiles MDX de usuarios en un servidor con privilegios.
- No uses
evaluatecon contenido remoto sin aislamiento. - Restringe imports y componentes disponibles si el contenido es semitrustado.
- Aísla el proceso (contenedor, sandbox, límites de recursos) si no hay alternativa.
- Aplica CSP y evita secretos en el entorno de compilación.
- Para contenido generado por usuarios, usa Markdown + sanitización, no MDX.
Véase también la sección 29.17.
48.9 Dependencias y cadena de suministro {#489-dependencias-y-cadena-de-suministro}
- Fija versiones y usa un archivo de bloqueo (
package-lock.json,pnpm-lock.yaml,yarn.lock). - Ejecuta
npm audito el equivalente de tu gestor. - Activa Dependabot o Renovate para actualizaciones controladas.
- Revisa los plugins que procesan Markdown: se ejecutan durante el build.
- Desconfía de paquetes con nombres similares a los oficiales (typosquatting).
- Verifica la procedencia de temas, componentes y acciones de CI.
- En GitHub Actions, fija las acciones por SHA cuando sea posible.
48.10 Configuración segura por herramienta {#4810-configuración-segura-por-herramienta}
| Herramienta | Configuración recomendada |
|---|---|
| markdown-it | html: false; sanitizar si se activa HTML; cuidado con linkify |
| remark/rehype | rehype-sanitize en la cadena; no confiar en HTML crudo |
| cmark | No usar --unsafe con contenido no confiable |
| GitHub/GitLab | La plataforma sanea, pero no subas secretos ni confíes en HTML embebido |
| Pandoc | Usar --sandbox si se procesan documentos no confiables; revisar filtros Lua |
| MDX | Solo contenido de confianza; aislar la compilación; CSP |
| Sitios estáticos | CSP, rel="noopener noreferrer", imágenes locales, dependencias auditadas |
pandoc --sandboxrestringe el acceso al sistema de archivos, pero no convierte a Pandoc en un entorno completamente seguro para código arbitrario. Los filtros y las plantillas también deben revisarse.
48.11 Checklist de seguridad {#4811-checklist-de-seguridad}
- El HTML está desactivado o sanitizado.
- Los protocolos de URL están en allowlist.
- Los enlaces externos usan
rel="noopener noreferrer". - Las imágenes remotas están controladas o proxificadas.
- No se compila MDX de usuarios.
- Las dependencias están fijadas y auditadas.
- Hay una CSP en el sitio final.
- No hay secretos en el repositorio ni en el entorno de build.
- Los plugins y filtros se revisan antes de usarlos.
- El contenido no confiable se trata como no confiable de extremo a extremo.