Artículos en profundidad sobre la tecnología que da forma al futuro.

Backdoor en Cursor: los forks de VS Code y la confianza

La backdoor en la extensión Claude Code del Marketplace de Cursor muestra que los forks de VS Code no heredan la confianza de Microsoft. Qué revisar.

Un portátil mostrando una cuadrícula de fichas de extensiones, una agrietada con luz roja y una sombra que alcanza el teclado
La cuadrícula de extensiones se ve idéntica tanto si la ficha de detrás está limpia como si lleva una carga maliciosa.

Un desarrollador abre Cursor un martes por la mañana, ve una actualización de la extensión Claude Code y la deja instalarse. No parece nada distinto. La extensión se carga, el asistente de IA funciona y, para la hora de comer, la máquina tiene una clave Ed25519 del atacante en ~/.ssh/authorized_keys, un túnel SSH inverso que envía beacons a un servidor en internet y un LaunchAgent disfrazado de proceso del sistema de Apple que mantiene todo vivo tras cada reinicio. Esa es la backdoor del Marketplace de Cursor que divulgó Phorion, y es la demostración más limpia hasta ahora de algo que los que trabajamos en seguridad venimos murmurando desde hace años: los forks de VS Code heredan la base de código de Microsoft, pero no su modelo de confianza.

He pasado buena parte de mi carrera haciendo respuesta a incidentes en máquinas de desarrolladores, y este incidente me toca porque el mecanismo de entrega fue de lo más normal. Nada de phishing. Ningún zero-day. Solo un paquete de extensión manipulado, versiones 2.1.312 y 2.1.313, publicado en un marketplace en el que los desarrolladores confían por defecto porque se parece a lo que ya confían. Los canales propios de Anthropic estaban limpios: alguien usó acceso al pipeline de publicación de Cursor para subir una copia modificada de una extensión que Anthropic nunca compiló para ese registro. El radio de impacto fue cada usuario de Cursor que se actualizó automáticamente en una ventana de once horas.

Qué hacía realmente la backdoor

El implante era una expresión de función invocada inmediatamente (IIFE) antepuesta al bundle minificado extension.js. Todo lo que venía después del prefijo inyectado parecía la extensión normal: los mismos exports activate y deactivate, el mismo código de UI, las mismas cadenas. Esa es la parte que debería preocuparte si tu estrategia de detección es «comparar el bundle con el upstream»: la parte maliciosa son unos cientos de bytes de JavaScript extra sobre un archivo legítimo de megas. Se ejecuta en cuanto Node carga el módulo, antes de que se dispare el callback activate de la propia extensión. El usuario no tiene que hacer clic en nada, invocar a Claude ni interactuar con la extensión.

La carga útil es un trabajo de cuatro etapas, y, siendo honestos, está bien hecho:

  • Siembra de claves. Añade una clave pública Ed25519 controlada por el atacante, comentada como svc-telemetry, a ~/.ssh/authorized_keys con los permisos correctos 0700/0600.
  • Activación de SSH. Intenta asegurarse de que sshd esté en ejecución: systemctl en Linux, Remote Login en macOS, OpenSSH Server mediante PowerShell en Windows.
  • Túnel inverso. Abre una conexión SSH saliente a un servidor controlado por el operador con -R *:0:localhost:22, de modo que el atacante puede llegar de vuelta a través del túnel al puerto 22 de la víctima e iniciar sesión con la clave sembrada. No hace falta un protocolo C2 propio: SSH es el C2.
  • Persistencia y reconocimiento. Un LaunchAgent de usuario llamado com.apple.sysmond.plist en macOS, una entrada @reboot de cron en Linux y un respaldo en la carpeta de Inicio en Windows. Después, un beacon único con el hostname, el usuario, el email de git, la IP pública y hasta cuarenta nombres de variables de entorno que coincidan con /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.

Ese último punto es ingenioso de una forma que merece entenderse. El beacon no exfiltra tu clave secreta de AWS: exfiltra el hecho de que tienes una. Es un inventario de objetivos. El operador obtiene un menú de qué máquinas comprometidas pertenecen a ingenieros cloud con credenciales activas en el entorno, que es exactamente a quién vuelves primero a través del túnel.

El bug de macOS que salvó a la gente sin querer

Este es el detalle que, como respondedor de incidentes, me parece oscuramente gracioso: la ruta de activación en macOS del malware está rota por un error clásico de scripting. El código ejecuta systemsetup -getremotelogin como usuario sin privilegios e intenta activar Remote Login solo si la salida coincide con /Off/i. Pero en macOS, ese comando se niega a ejecutarse sin acceso de administrador. Imprime «You need administrator access to run this tool... exiting!» en stdout y termina con código de estado 0.

// what the malware effectively does
var rl = ex("systemsetup -getremotelogin 2>/dev/null", 4000);
if (/Off/i.test(rl)) {
det("sudo -n systemsetup -setremotelogin on >/dev/null 2>&1");
}

La cadena de error no contiene «Off», así que la expresión regular falla, así que el paso de activación nunca se ejecuta. El script trata «no tengo permisos para comprobar si SSH está apagado» como «SSH ya está encendido». Un código de salida 0 de un comando fallido es una de las trampas más antiguas del scripting en shell, y esta vez le explotó al autor del malware en lugar de a un administrador de sistemas. En un Mac sin Remote Login ya activado, el túnel inverso se conecta sin problemas, pero cualquier cosa reenviada de vuelta choca con un puerto 22 cerrado. Las claves y la persistencia sí se depositan —la máquina sigue comprometida—, pero la vía de acceso interactivo está muerta al nacer.

No te consueles con esto. Si el usuario ya tenía Remote Login activado, o lo activa más adelante, la clave sembrada en authorized_keys está ahí esperando. La ruta de Windows tiene un bug similar (canaliza un comando de PowerShell a través de un helper que tiene /bin/sh codificado), y la lógica de cron en Linux puede borrar el crontab existente de un usuario en ejecuciones repetidas. El malware escrito por humanos tiene bugs humanos. El del mes que viene no los tendrá.

Por qué los marketplaces bifurcados no pueden heredar el modelo de confianza de Microsoft

Este es el punto estructural, y es el que quiero que los responsables de ingeniería mediten. Cuando instalas una extensión del Visual Studio Marketplace oficial, hay un pipeline real detrás: cuentas de editor con verificación de dominio, escaneo al publicar, procesos de retirada y una empresa con un departamento legal al otro lado. Está lejos de ser perfecto —extensiones maliciosas se han colado—, pero es un sistema.

Los forks de VS Code como Cursor no pueden usar ese marketplace por razones de licencia, así que apuntan a registros alternativos: Open VSX o infraestructura propia. La interfaz del editor es idéntica byte a byte. La página de la extensión muestra el mismo nombre, el mismo icono y un nombre de editor plausible. Pero cada control que hacía confiable al marketplace original es una reimplementación o directamente no existe. ¿Quién verificó al editor? ¿Quién escaneó el bundle? ¿Se compiló el artefacto desde el repositorio de código que afirma, y puedes comprobarlo? En este incidente, las versiones manipuladas probablemente nunca existieron en el registro de Microsoft; solo vivían en el del fork. La confianza era puramente visual.

Ya vimos la misma película en el mundo de los registros de paquetes. La compromisión de LiteLLM en PyPI funcionó porque el punto débil es el acceso a la publicación, no el código. Las extensiones son peores que los paquetes en un aspecto importante: no hay cultura de lockfile, ni reproducibilidad con npm ci, ni pinning por hash. El editor se actualiza en silencio y, como descubrió un comentarista de Hacker News, el retraso de auto-actualización de VS Code ni siquiera se aplica a los «editores de confianza», una lista que no puedes configurar. Tu conjunto de extensiones es un árbol de dependencias vivo y sin fijar, que se ejecuta con todos tus privilegios de usuario y se actualiza solo mientras duermes.

Dos escaparates idénticos, uno sólido y otro revelado como una fachada hueca sostenida por detrás
Un marketplace bifurcado se ve idéntico desde el frente; la infraestructura de confianza que hay detrás puede ser una fachada.

Las herramientas de codificación con IA son una escalada de privilegios que tú elegiste

Quita el drama del marketplace y mira cuál era realmente el objetivo. Una extensión de codificación con IA no es un tema de color. Se ejecuta con los permisos del IDE, que son tus permisos, y su trabajo completo es leer tu árbol de código, guardar tus credenciales de API y, cada vez más, ejecutar comandos de shell en tu nombre. Cuando escribimos sobre cómo las CLI de codificación agéntica gestionan los permisos, el tema recurrente era que el agente útil y el peligroso se separan por política, no por capacidad. Una extensión que puede ejecutar child_process está a una mala publicación de ejecutar cualquier cosa.

Un comentarista lo dijo sin rodeos: quienes usan estas herramientas han estado ejecutando curl-pipe-bash a diario, y pasamos una década diciéndole a los juniors que no lo hicieran. Es una exageración, pero no mucho. La diferencia entre curl | bash y una extensión agéntica con actualización automática es que la extensión tiene una UI más bonita. He defendido antes que las herramientas CLI no deberían instalar daemons en segundo plano por defecto; este incidente es lo que pasa cuando el daemon lo instala otra persona y tú nunca diste tu consentimiento. Un proceso ssh desacoplado que sobrevive al cierre del IDE es exactamente el patrón de comportamiento que marco en las revisiones, y aquí lo entregó un atacante a través de un canal de actualización de confianza.

Cómo revisar tu flota ahora mismo

Si alguien en tu organización usa Cursor, trátalo como un incidente activo, no como una noticia. Los indicadores de alta señal, directamente del informe forense:

# macOS / Linux: the planted key is the cleanest signal
grep -s svc-telemetry ~/.ssh/authorized_keys && echo "IMPACTED" || echo "key not found"
# Check persistence artifacts
ls ~/Library/LaunchAgents/com.apple.sysmond.plist 2>/dev/null   # macOS
ls ~/.config/.smt ~/.config/.smtk 2>/dev/null                   # macOS + Linux
crontab -l 2>/dev/null | grep -F .smt                           # Linux @reboot entry
# Windows (PowerShell): check both authorized_keys locations
Select-String svc-telemetry "$HOME\.ssh\authorized_keys",
"C:\ProgramData\ssh\administrators_authorized_keys" -ErrorAction SilentlyContinue
# Live tunnel: look for detached ssh with remote forwarding
ps aux | grep -E 'ssh.*-R' | grep -v grep

Algunas notas de caza por experiencia. Primero, verifica la huella de la clave, no solo el comentario: el implante deduplica por la cadena de comentario svc-telemetry, y también podría hacerlo un intento de limpieza. Segundo, el archivo de la clave del túnel vive en el directorio temporal con un nombre como .stk_ seguido de diez caracteres hexadecimales derivados de un MD5 del hostname más la ruta del home; es determinista por máquina, así que puedes calcular el nombre exacto y buscarlo. Tercero, la telemetría de procesos del EDR no mostrará el intento de activación en macOS para usuarios estándar, porque por el bug nunca llega a ejecutarse. La ausencia de systemsetup -setremotelogin en tus logs no significa ausencia de compromiso. La clave y el LaunchAgent son la verdad de referencia.

Si encuentras la clave, la máquina pasa por el tratamiento completo de respuesta a incidentes: rota cada credencial cuyo nombre de variable de entorno pudo haber sido enviado en el beacon (que son casi todas; la regex captura AWS, GCP, cualquier cosa con KEY o TOKEN en el nombre), revoca el acceso de la máquina y reinstálala desde cero. No te limites a borrar la clave y dar el asunto por cerrado. El operador tuvo, o intentó tener, acceso shell interactivo; no puedes enumerar lo que tocó solo con el código del implante. Aquí es también donde muerden los puntos ciegos del logging de autenticación en la nube: revisa tus logs de auditoría cloud en busca de uso de consola o API desde la ventana entre la actualización de la extensión y la divulgación, filtrando por las credenciales del usuario afectado.

Un haz de linterna que destaca una llave con brillo rojo entre un llavero, con un cable perdiéndose en la oscuridad
Busca la clave plantada por su huella, no por su etiqueta: la etiqueta es lo primero que borra una limpieza.

Endurecimiento: contener el editor, no solo la extensión

Después del incidente es cuando todos preguntan qué cambiar. Mi postura, y la defiendo: nunca vas a auditar hasta llegar a un ecosistema de extensiones seguro, así que contienes. Cubrimos el compromiso general en contención frente a monitorización de agentes de IA, y este incidente es el caso práctico de contención. La monitorización no te dijo nada aquí hasta que una firma de seguridad externa tuvo la ocurrencia de diseccionar el bundle.

  • Fija las versiones de las extensiones y desactiva la actualización automática para cualquiera con permisos amplios, y aplica la misma regla en todos los forks. Sí, heredarás fatiga de actualizaciones. Es más barato que la respuesta a incidentes.
  • Filtrado de egreso en las estaciones de desarrollo. Todo el modelo de acceso aquí dependía de una conexión SSH saliente a una IP de VPS. Si tus máquinas de desarrollo no pueden abrir el puerto 22 saliente arbitrario, la etapa del túnel muere. Casi nadie lo hace, y habría neutralizado este implante por completo.
  • Vigila el estado de SSH como un canario. Alerta cuando Remote Login se active, aparezcan claves nuevas en authorized_keys o haya procesos sshd en máquinas que no deberían ejecutar sshd.
  • Inventaría tus herramientas de IA como inventarías servidores. Sabe qué extensiones y CLI de agentes están aprobadas, qué permisos tienen y de qué marketplace vienen. Si la respuesta a «¿de qué registro vienen las instalaciones de Cursor de nuestra empresa?» es encogerse de hombros, ese es el proyecto del próximo trimestre.
  • Prefiere agentes que se ejecuten en sandboxes o contenedores cuando sea viable, con credenciales inyectadas por sesión en lugar de ambientales en el entorno: el escaneo de nombres de variables del beacon funciona precisamente porque dejamos secretos tirados en los entornos de los procesos.

Un marketplace de extensiones sin su propio pipeline de verificación no es un marketplace: es un servidor de archivos con un botón de instalar.

La conclusión incómoda

Anthropic no fue vulnerada. Fue el marketplace de Cursor, y la marca de Anthropic absorbió el daño igualmente, porque para un desarrollador la extensión es el producto, sin importar quién publicó cada build. Esa es la economía de los ecosistemas bifurcados: la forma más barata de golpear un objetivo de alto valor es atacar el registro más débil en el que confían sus usuarios. Los asistentes de codificación con IA están justo en la intersección perfecta: enorme base instalada, máximo privilegio local, credenciales por todas partes y una cadena de suministro que bifurca y re-aloja sus artefactos en registros con posturas de seguridad muy dispares.

Este caso terminó con las versiones maliciosas retiradas en unas once horas y un bug de macOS que limitó el daño. La respuesta fue correcta. El problema es la arquitectura, y no ha cambiado: tu editor es un gestor de paquetes que ejecuta todo lo que instala como tú, se actualiza en silencio y, en un número creciente de configuraciones, obtiene sus paquetes de un registro que nadie de tu equipo de seguridad ha evaluado jamás. Corrige las reglas de egreso, fija las extensiones e inventaría los agentes. La próxima compromisión de un publicador no tendrá un bug de regex salvando a tus usuarios.