Checklist de fortificación
Ajustes accionables para un despliegue en producción. Cada punto dice qué hacer, por qué y cómo verificarlo. Lee antes el modelo de amenazas para saber qué puntos son barreras reales y cuáles son higiene.
Una configuración base
Sección titulada «Una configuración base»import { wu } from 'wu-framework';
await wu.init({ sandbox: 'strict', strictFallback: false, // el valor por defecto; sé explícito overrides: { enabled: false }, // sin redirección por cookie en producción apps: [ { name: 'cart', url: 'https://cart.example.com' }, { name: 'billing', url: 'https://billing.example.com', roles: ['admin'] }, ],});
wu.eventBus.enableStrictMode();wu.eventBus.registerApp('cart', { permissions: ['cart:*'] });wu.eventBus.registerApp('billing', { permissions: ['billing:*'] });
wu.setPrincipal({ role: currentUser.role, permissions: currentUser.permissions });
await wu.mount('cart', '#cart');
// Falla de forma ruidosa si el aislamiento no es el que pedimos.const info = wu.getSandboxInfo('cart');if (info?.actualMode !== 'strict') { throw new Error(`cart is running as ${info?.actualMode}, not strict`);}Transporte y contenido
Sección titulada «Transporte y contenido»- Todo por TLS. El shell, cada URL de app, cada manifiesto. El contenido
mixto bloquea el import del iframe en modo
stricty hace fallar el mount. - Sirve el JavaScript con un content type de JavaScript. La sonda de
rutas de Wu rechaza
text/htmlprecisamente para detectar fallbacks de SPA disfrazados de bundles; un host que sirve.jscomotext/htmlrompe la resolución. - Publica
wu.jsonen producción, con unentryque use un prefijo de carpeta conocido (src/,dist/,public/,build/,assets/,lib/,es/). Esto evita la sonda de 8 peticiones y el críptico error de MIME. - Limita CORS al origen del shell, no a
*.
Access-Control-Allow-Origin: https://shell.example.comVary: OriginContent Security Policy
Sección titulada «Content Security Policy»Wu no necesita unsafe-eval desde v2.7. Si lo añadiste por wu, quítalo.
Content-Security-Policy: default-src 'self'; script-src 'self' https://cart.example.com https://billing.example.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://cart.example.com https://billing.example.com; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';Notas:
- Fija los orígenes de las apps explícitamente en
script-srcyconnect-src. Un comodín ahí anula casi todo el valor. - Distingue CSR de SSR en
style-src. El montaje cliente inyecta elementos<style>en runtime y todavía requiere'unsafe-inline'. En SSR, los estilos declarativos sí aceptan el nonce generado porwu-framework/server, por lo que esa respuesta no necesita la excepción. -
script-src 'unsafe-inline'solo hace falta si una app en modoevalsirve bloques<script>inline. Los scripts externos no necesitan excepción. -
frame-src 'self'cubre los iframes de sandbox ocultos del mismo origen de wu. Añade tu host de sandbox de origen cruzado si usas uno. - No añadas
'unsafe-eval'. Si algo lo exige, no es wu.
SSR sin unsafe-inline
Sección titulada «SSR sin unsafe-inline»import { createNonce, applySecurityHeaders, renderApps, renderStateScript,} from 'wu-framework/server';
const nonce = createNonce(); // uno nuevo por respuestaconst { html, state } = await renderApps(apps, { nonce });applySecurityHeaders(res, { nonce });res.end(template .replace('<!--apps-->', html) .replace('<!--state-->', renderStateScript(state, { nonce })));El mismo nonce debe llegar a la CSP, los <style> de los shadow roots y el
<script> de estado. Los valores malformados lanzan antes de interpolarse.
No reutilices el nonce ni caches el HTML que lo contiene.
Sandbox
Sección titulada «Sandbox»- Deja
strictFallbackenfalse. Es el valor por defecto desde v2.7. Activarlo deja que un fallo de CORS degrade una app que aislaste deliberadamente. - Verifica
actualModedespués de cada mount de cualquier cosa que no controles del todo, en producción, no solo en los tests. - No trates
strictcomo una frontera contra código hostil. Para código no confiable usa un iframe de origen cruzado consandbox="allow-scripts"y sinallow-same-origin, más un puentepostMessagevalidado. Mira Apps de terceros.
const REQUIRED = { cart: 'strict', billing: 'strict', legacy: 'eval' };
for (const [name, expected] of Object.entries(REQUIRED)) { const info = wu.getSandboxInfo(name); if (!info) continue; // aún no montada if (info.actualMode !== expected) { report(`${name}: expected ${expected}, got ${info.actualMode}`); }}Bus de eventos
Sección titulada «Bus de eventos»- Activa
strictMode. Es automático cuandoprocess.env.NODE_ENV === 'production', una constante de tiempo de build. Si tu bundler no la reemplaza, llama awu.eventBus.enableStrictMode()explícitamente. - Restringe los permisos por app.
registerAppusa['*']por defecto, lo que deja a cualquier app registrada emitiruser:logout.
wu.eventBus.registerApp('cart', { permissions: ['cart:*'] });- Comprueba el valor de retorno de
emit()donde importe. Una emisión rechazada devuelvefalse; no lanza error. - No pongas secretos en los payloads de los eventos. Cualquier app de la
página puede suscribirse con
wu.on('*'), y el historial guarda los últimos 100 eventos. - Recuerda que los tokens son etiquetas, no capacidades. Cualquier código en tu ventana puede leerlos.
- No pongas credenciales, tokens de sesión ni datos personales en el store
compartido. Es legible por cada app, y
store.sync()los difundirá tan contento entre pestañas o por un WebSocket. - Valida antes de escribir rutas construidas con entrada del usuario. La
guarda contra contaminación de prototipos lanza error ante
__proto__,constructoryprototype, pero no evitará queset('a.' + userInput, v)pisotee estado no relacionado. - Si usas
store.sync(), trata el transporte como hostil. Las operaciones remotas pasan por la guarda de contaminación, pero todo lo demás que llegue se convierte en estado. Termina la sincronización por WebSocket con autenticación en tu servidor.
- Declara
rolesen las apps privilegiadas, en la configuración dewu.init()o en elwu.jsonde la app. - Establece un principal al iniciar sesión y límpialo al cerrarla.
- Aplica en el servidor. El RBAC del cliente decide qué renderizar. Que el servidor decida qué servir es el control de verdad.
wu.setPrincipal({ role: 'admin', permissions: ['mount:*'] });wu.setPrincipal(null); // al cerrar sesión
if (wu.can('billing')) await wu.mount('billing', '#billing');Gestiona la denegación para la UX:
window.addEventListener('wu:access:denied', (e) => { showAccessDenied(e.detail.appName, e.detail.required);});Overrides de QA
Sección titulada «Overrides de QA»- Desactívalos explícitamente en producción:
overrides: { enabled: false }. La autodetección ya trata cualquier hostname no local como producción, pero unfalseexplícito no lo puede deshacer un cambio de configuración que no notaste. - Nunca publiques un
allowedDomainsvacío con los overrides activados. Vacío significa permitir todo, lo que convierte una cookie en un cargador de scripts arbitrario. - Nunca desactives
showIndicator. El banner es el control anti-phishing. - Considera exigir además una sesión de staff, para que una cookie por sí sola no baste:
overrides: { enabled: currentUser?.isStaff === true, allowedDomains: ['*.preview.example.com'], showIndicator: true,}Manifiestos y cadena de suministro
Sección titulada «Manifiestos y cadena de suministro»- Genera
wu.jsondesde el build, para queentrysiempre coincida con el bundle con hash. Los hashes mantenidos a mano se desincronizan. - Trata el compromiso del CDN de una app como un compromiso de tu página. Wu no verifica SRI ni hashes.
- Fija los orígenes de las apps en la CSP para que un secuestro de DNS no pueda sustituir el host.
- Mantén los manifiestos pequeños y aburridos.
normalize()descarta los campos extra de todos modos.
Hooks y plugins
Sección titulada «Hooks y plugins»- Los hooks son fail-closed desde v2.7 (fallan cerrando: cancelan en vez
de continuar): un middleware que lanza antes de
next()cancela la operación. Eso es lo que quieres en un guardián. - Marca los hooks de instrumentación con
{ failOpen: true }para que un fallo de métricas no bloquee un mount. - Audita los plugins. Un plugin corre en el shell, con acceso completo. Es código del shell, con el modelo de amenazas del shell.
wu.hooks.use('beforeMount', async (ctx, next) => { if (!(await serverSaysYes(ctx.appName))) return; // sin next() = cancelar await next();}, { priority: 100 });
wu.hooks.use('afterMount', reportMetrics, { failOpen: true });Capa de IA
Sección titulada «Capa de IA»Si usas wu.ai:
- Nunca pongas una clave de API en el navegador. Apunta el proveedor a tu propio endpoint proxy, que guarda la credencial en el servidor.
- Mantén activada la confirmación de herramientas para cualquier cosa con efectos secundarios.
- Trata la salida del modelo como entrada no confiable. Una llamada a una herramienta es una petición, no una autorización.
Mira Capas de seguridad de IA.
Verificar en CI
Sección titulada «Verificar en CI»Convierte la checklist en una aserción que corra en cada despliegue:
test('production hardening', async ({ page }) => { await page.goto('/');
const state = await page.evaluate(() => ({ apps: wu.inspect().apps.map((a) => ({ name: a.name, sandbox: a.sandbox })), overrides: wu.overrides.getStats(), eventStats: wu.eventBus.getStats(), }));
for (const app of state.apps) { expect(app.sandbox.actualMode).toBe(app.sandbox.requestedMode); } expect(state.overrides.enabled).toBe(false);});Y comprueba las cabeceras en sí:
test('CSP has no unsafe-eval', async ({ request }) => { const res = await request.get('/'); const csp = res.headers()['content-security-policy'] ?? ''; expect(csp).not.toContain('unsafe-eval'); expect(csp).toContain("object-src 'none'");});La versión corta
Sección titulada «La versión corta»Si solo vas a hacer cinco cosas:
strictFallback: false(el valor por defecto) y verificagetSandboxInfo()después del mount.- Sin
unsafe-evalen tu CSP, y fija los orígenes de las apps. eventBus.strictModeactivado, con permisos estrechos por app.overrides: { enabled: false }en producción.- Aplica la autorización en el servidor. Todo lo del cliente, RBAC incluido, es una decisión de renderizado.
Y acepta la restricción que da forma a todo lo demás: para código genuinamente no confiable, ningún modo de sandbox de wu es una frontera. Usa un iframe de origen cruzado.
