Ir al contenido

Apps de terceros

“App de terceros” cubre dos situaciones muy distintas, y Wu solo puede ayudarte con una de ellas.

Código de otro equipo Código de otra empresa
Puedes leerlo normalmente sí normalmente no
Puede romper el tuyo por accidente
Puede atacarte a propósito no asume que sí
Un modo de sandbox de wu alcanza

Si estás en la columna izquierda —un equipo socio, un desarrollo de una agencia, el frontend de un producto adquirido— sandbox: 'strict' es la herramienta correcta. Si estás en la columna derecha, sigue leyendo hasta el final: ningún modo de sandbox de wu contiene código hostil, y creer lo contrario es lo más peligroso que podrías llevarte de esta página.

El riesgo son los accidentes: un global que sobrescriben, un prototipo que parchean, un intervalo que olvidan limpiar, un crash que se lleva la página por delante.

await wu.init({
apps: [
{
name: 'partner-widget',
url: 'https://partner.example.com',
sandbox: 'strict',
strictFallback: false, // el valor por defecto desde v2.7 — sé explícito igual
},
],
});
await wu.mount('partner-widget', '#widget');
const info = wu.getSandboxInfo('partner-widget');
if (info?.actualMode !== 'strict') {
throw new Error(`Expected iframe isolation, got ${info?.actualMode}`);
}

Lo que eso te da:

  • Globales separados. Su window.React no es tu window.React. Sus parches de prototipos se quedan en su realm.
  • Teardown garantizado. Destruir el iframe mata el realm, y además Wu limpia cada timer registrado con las propias funciones de limpieza del iframe.
  • Un wu restringido. Dentro del iframe, window.wu es una fachada congelada con la API pública solamente: sin wu.core, sin wu.cache, sin wu.pluginSystem.
  • Contención de CSS. Sus estilos no pueden llegar a tu página, y con styleMode: 'isolated' los tuyos no pueden llegar a la suya.

Combínalo con el resto de la superficie de contención:

await wu.init({
apps: [{ name: 'partner-widget', url: '', sandbox: 'strict', roles: ['staff'] }],
overrides: { enabled: false }, // sin redirección por cookie en producción
});
wu.eventBus.enableStrictMode();
wu.eventBus.registerApp('partner-widget', {
permissions: ['partner:*'], // solo puede emitir en su propio namespace
});

Los permisos de eventos son la parte infrautilizada. Por defecto registerApp concede ['*']; restringirlo significa que el widget no puede emitir user:logout y disparar el cierre de tu sesión.

Aquí está el límite honesto.

strict y eval ejecutan el código de la app en un iframe del mismo origen, y el document.createElement de ese iframe se redirige deliberadamente al documento anfitrión; si no, React y Vue tropiezan con discrepancias de ownerDocument al insertar nodos en el shadow root. Lo que significa:

// Desde dentro de cualquier sandbox de wu:
document.createElement('div').ownerDocument.defaultView.wu // el wu real

Wu enmascara window.parent, window.top y window.frameElement, pero este camino está abierto por diseño. Mismo origen es mismo origen: el iframe también puede alcanzar localStorage, sessionStorage, las cookies y IndexedDB de tu origen, y puede hacer peticiones con credenciales a tu API.

Un modo de sandbox de wu es contención para código que controlas. No es una frontera de seguridad. Mira el modelo de amenazas.

Un iframe de origen cruzado. No “otra ruta”, no “otro puerto”: un origen distinto cuyo HTML controlas tú.

<!-- En el shell, en app.example.com -->
<iframe
src="https://sandbox.example.net/host.html?app=untrusted"
sandbox="allow-scripts"
referrerpolicy="no-referrer"
allow=""
style="border:0;width:100%;height:400px">
</iframe>

Con sandbox="allow-scripts" y sin allow-same-origin, el frame obtiene un origen opaco: sin acceso a tus cookies, tu almacenamiento, tu DOM ni tu window.wu. Ojo: allow-scripts junto con allow-same-origin echa por tierra todo el ejercicio, porque el frame puede entonces quitarse su propio atributo sandbox.

Sirve sandbox.example.net desde un origen aparte (un dominio separado es mejor que un subdominio, porque las cookies pueden alcanzar el dominio padre), y ejecuta wu dentro de ese frame si también quieres el modelo de micro-apps allí.

Después construye el puente de forma deliberada:

// Lado del shell — valida cada mensaje, sin excepciones
const FRAME_ORIGIN = 'https://sandbox.example.net';
window.addEventListener('message', (event) => {
if (event.origin !== FRAME_ORIGIN) return;
if (event.source !== frame.contentWindow) return;
const msg = event.data;
if (msg?.type !== 'cart:add' || typeof msg.sku !== 'string') return;
// Vuelve a autorizar de este lado. Lo que afirma el frame es entrada, no permiso.
if (!currentUser.canAddToCart) return;
cart.add(msg.sku);
});

El puente es el modelo de seguridad. Cada capacidad que tiene el código no confiable es un tipo de mensaje que tú decidiste implementar, comprobado contra permisos que evalúas del lado confiable. Nada es implícito.

Añade una CSP que limite adónde puede ir el frame:

Content-Security-Policy:
frame-src https://sandbox.example.net;
script-src 'self';

Incluso un socio que se porta bien publica desde un servidor que tú no operas. Wu no verifica hashes de bundles: descarga lo que sea que devuelva la URL.

  • Sirve todo por TLS y fija los orígenes exactos en tu CSP.
  • Considera Subresource Integrity si puedes controlar la URL de entrada y esta lleva hash de contenido. Wu no calculará ni comprobará SRI por ti.
  • Trata el compromiso del CDN de un socio como equivalente al compromiso de tu propia página, porque en modo module es exactamente eso.
¿El código es adversario, o solo ajeno?
├─ Ajeno (otro equipo, una agencia, un proveedor con el que tienes contrato)
│ → sandbox: 'strict', strictFallback: false
│ → styleMode: 'isolated' en el wu.json de la app
│ → eventBus.registerApp con permisos estrechos
│ → verifica con getSandboxInfo()
└─ Adversario (plugins enviados por usuarios, un marketplace no confiable,
cualquier cosa que no ejecutarías en tu propio build)
→ iframe de origen cruzado, sandbox="allow-scripts" (sin allow-same-origin)
→ puente postMessage con validación de origen + source + forma
→ CSP frame-src fijando el origen del sandbox
→ wu es opcional dentro de ese frame; no es él la frontera
  • sandbox: 'strict' puesto, strictFallback dejado en false
  • getSandboxInfo() verificado después del mount, también en producción
  • styleMode: 'isolated' en el manifiesto de la app
  • eventBus.enableStrictMode() y permisos estrechos por app
  • overrides: { enabled: false } en producción
  • roles de RBAC en todo lo privilegiado, con la comprobación real en el servidor
  • CSP connect-src / script-src fijados a los orígenes que esperas
  • El origen de la app envía cabeceras CORS, para que strict no lance error al montar

Mira la Checklist de fortificación para la lista completa.