Informe de auditoría
Aplicación: http://localhost:8099/
Fecha: 2026-08-16
Alcance: Análisis automatizado y no destructivo del frontend público y de la API de datos, más la calidad del sitio y su visibilidad ante las IA. No incluye pruebas de intrusión manuales ni revisión del código de servidor.
Resumen ejecutivo
REQUIERE ATENCIÓN URGENTE antes de seguir en producción.
| Severidad | Hallazgos |
|---|---|
| 🔴 Crítico | 0 |
| 🟠 Alto | 1 |
| 🟡 Medio | 2 |
| ⚪ Bajo | 4 |
Aparte, se encontraron 12 problema(s) de calidad del sitio — están en la sección de abajo. No afectan al veredicto de seguridad.
Hallazgos detallados
1. 🟠 ALTO — Token bearer hardcodeado en el código del cliente
Clasificación: CWE-798
Dónde: http://localhost:8099/assets/app.js
Evidencia: Authorization: Bearer 7d3f1a9c…d2b6
Cómo se explota: La clave viaja en el JavaScript del cliente, así que cualquiera puede abrir el código de la página, copiarla y llamar al proveedor haciéndose pasar por ti — sin autenticarse. El token está en un fichero que el sitio sirve a todo el mundo, así que todos los visitantes tienen la misma credencial. Cualquiera que abra la pestaña de red puede reutilizarlo contra las rutas de API que autoriza.
Por qué importa: Un token bearer compilado en el bundle es un secreto compartido que se le entrega a cada visitante, y no se puede revocar para uno sin revocarlo para todos. Que dé acceso a algo depende de lo que haga el servidor con él — pero un token estático en código de cliente no es autenticación, y si es la única comprobación, la API es pública de hecho.
Referencias: https://cwe.mitre.org/data/definitions/798.html · https://cwe.mitre.org/data/definitions/798.html
Punto 1 de la checklist.
2. 🟡 MEDIO — Falta la cabecera Content-Security-Policy
Clasificación: CWE-693
Evidencia: Ausente en la respuesta
Cómo se explota: Sin CSP, un script inyectado o de terceros puede cargarse y ejecutarse desde cualquier origen, y sacar de ahí tokens o datos de usuarios.
Por qué importa: Falta la principal defensa en profundidad contra el cross-site scripting.
Referencias: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP · https://cwe.mitre.org/data/definitions/693.html
Punto 22 de la checklist.
3. 🟡 MEDIO — Source map expuesto — el código fuente original se puede descargar
Clasificación: CWE-540
Dónde: http://localhost:8099/assets/app.js.map
Evidencia: http://localhost:8099/assets/app.js.map se sirve públicamente e incrusta el fuente original completo (sourcesContent).
Cómo se explota: Cualquiera abre http://localhost:8099/assets/app.js.map y reconstruye el fuente sin minificar — nombres de componentes, comentarios, configuración en línea y cualquier valor hardcodeado que el bundler dejara dentro.
Impacto: El código fuente original es legible por cualquiera, lo que convierte una caja negra en un libro abierto: cualquier otra debilidad pasa a ser mucho más fácil de encontrar, y los secretos que quedaran en el código se entregan directamente.
Por qué importa: Las builds de producción no deberían publicar source maps. Desactiva su emisión en producción, o elimina los ficheros .map de la salida desplegada.
Referencias: https://cwe.mitre.org/data/definitions/540.html · https://webpack.js.org/configuration/devtool/
Punto 23 de la checklist.
4. ⚪ BAJO — Falta la cabecera Strict-Transport-Security (HSTS)
Clasificación: CWE-319
Evidencia: Ausente en la respuesta
Cómo se explota: Un atacante en la ruta de red puede forzar a la víctima a HTTP plano en la primera petición e interceptar credenciales o cookies de sesión.
Por qué importa: HTTPS no queda fijado para las visitas siguientes, así que el navegador está dispuesto a volver a probar HTTP plano la próxima vez. La cabecera le dice que no lo haga nunca durante un plazo declarado — Strict-Transport-Security: max-age=63072000; includeSubDomains es el valor habitual, y va en el CDN o en el servidor, no en la página.
Referencias: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
Punto 22 de la checklist.
5. ⚪ BAJO — Falta la cabecera X-Frame-Options
Clasificación: CWE-1021
Evidencia: Ausente en la respuesta
Cómo se explota: La aplicación se puede incrustar en un iframe oculto dentro de una página maliciosa para que el usuario pulse acciones sin saberlo (clickjacking).
Por qué importa: No hay protección contra el enmarcado (también se puede fijar con frame-ancestors en la CSP).
Referencias: https://cwe.mitre.org/data/definitions/1021.html
Punto 22 de la checklist.
6. ⚪ BAJO — Falta la cabecera X-Content-Type-Options
Clasificación: CWE-430
Evidencia: Ausente en la respuesta
Cómo se explota: El navegador puede deducir por los bytes un tipo distinto del declarado (MIME sniffing), lo que habilita algunos ataques de ejecución de scripts.
Por qué importa: Sin X-Content-Type-Options: nosniff el navegador puede desconfiar del Content-Type declarado y tratar un fichero como lo que parezcan sus bytes. El caso clásico es una imagen subida que además se interpreta como script. Es una cabecera con un valor y sin contrapartidas.
Referencias: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
Punto 22 de la checklist.
7. ⚪ BAJO — La cabecera server revela la tecnología
Clasificación: CWE-200
Evidencia: server: SimpleHTTP/0.6 Python/3.9.6
Cómo se explota: Revelar el framework o el servidor y su versión permite a un atacante buscar CVEs conocidos de ese stack exacto.
Por qué importa: Fuga de información — oculta la cabecera en tu hosting o en tu framework.
Referencias: https://cwe.mitre.org/data/definitions/200.html
Punto 23 de la checklist.
Calidad del sitio y visibilidad ante las IA
Estos hallazgos no afectan al veredicto de seguridad ni al código de salida. Tratan de cómo leen este sitio los buscadores, las redes sociales y los asistentes de IA.
Huella de vibe-coding: 9/12 — El HTML no trae contenido — JavaScript lo renderiza todo · Sin meta description · Sin etiquetas Open Graph · Sin URL canónica · Sin datos estructurados · Sin sitemap.xml · Sin favicon · Sin lang en <html> · Sin llms.txt
5 de 17 comprobaciones superadas.
| Comprobación | Estado | Impacto | Nota |
|---|---|---|---|
| robots.txt deja entrar a los rastreadores de IA | ✅ ok | — | — |
| Rastreadores de IA bloqueados como se declaró | ⚪ no aplica | — | no se declaró intención de bloquear rastreadores de IA |
| El HTML trae contenido real | ❌ | 🔴 Impacto alto | — |
| Sin recursos rotos | ❌ | 🔴 Impacto alto | — |
| Sin errores de JavaScript al cargar | ⚪ no evaluada | — | no hay navegador disponible |
| Cada página tiene su propio <title> | ❌ | 🔴 Impacto alto | — |
| <title> presente | ✅ ok | — | — |
| Meta description | ❌ | 🟡 Impacto medio | — |
| Etiquetas Open Graph | ❌ | 🟡 Impacto medio | — |
| URL canónica | ❌ | 🟡 Impacto medio | — |
| Datos estructurados (JSON-LD) | ❌ | 🟡 Impacto medio | — |
| Exactamente un <h1> por página | ✅ ok | — | — |
| sitemap.xml | ❌ | 🟡 Impacto medio | — |
| llms.txt | ❌ | 🟡 Impacto medio | — |
| Atributo lang en <html> | ❌ | 🟡 Impacto medio | — |
| Las imágenes tienen texto alt | ❌ | 🟡 Impacto medio | — |
| Las URLs inexistentes devuelven 404 | ✅ ok | — | — |
| Favicon | ❌ | ⚪ Impacto bajo | — |
| JavaScript por debajo de 1 MB | ✅ ok | — | — |
Hallazgos del sitio en detalle
1. 🔴 Impacto alto — El HTML se sirve sin contenido
Evidencia: La página de inicio devuelve 90 caracteres de texto visible antes de que se ejecute JavaScript (2 de 2 páginas por debajo de 200 caracteres).
Impacto: Para todo lo que no ejecuta JavaScript el sitio es una página en blanco: sin titular, sin texto, sin nada que indexar ni citar. La mayoría de rastreadores de IA no ejecutan scripts, así que para ellos el sitio sencillamente no existe.
Por qué importa: Un shell renderizado en el cliente le entrega al rastreador un <div> vacío. Google renderiza tarde y de forma inconsistente; GPTBot, ClaudeBot y PerplexityBot no renderizan en absoluto. Cerrarlo es una decisión de arquitectura, no una edición de texto: o el titular y el primer párrafo reales viven dentro del nodo de montaje en index.html para estar en el fuente antes de que corra ningún script, o las páginas las genera un renderizador que emite HTML (Astro, React Server Components de Next.js, SSG de Vite).
Páginas: http://localhost:8099/, http://localhost:8099/app.html
2. 🔴 Impacto alto — Recursos referenciados por el sitio devuelven error
Evidencia: 1 fichero(s) referenciado(s) responden con 4xx/5xx — http://localhost:8099/logo.png → 404 (referenciado desde http://localhost:8099/)
Impacto: El visitante ve imágenes que faltan, secciones sin estilo o una función que nunca carga — la forma más rápida de parecer abandonado y perder la venta en la primera pantalla.
Por qué importa: La página apunta a ficheros que no están desplegados: una ruta que solo existe en local, un recurso renombrado o una imagen que el build nunca copió. Los recursos que se sirven desde la raíz tienen que vivir en public/ (Vite, Next.js, Astro) o en static/ (SvelteKit) para que el build los copie — si no, la referencia sobra.
3. 🔴 Impacto alto — Todas las páginas tienen el mismo <title>
Evidencia: Las 2 páginas rastreadas envían el mismo título: "Nimbus Invoices".
Impacto: Todas las páginas compiten por el mismo resultado de búsqueda y no gana ninguna. El título es la línea azul que la gente pulsa en Google y la etiqueta de cada pestaña y marcador compartido.
Por qué importa: Los buscadores tratan los títulos idénticos como páginas duplicadas: eligen una y descartan el resto. La causa habitual es un router de SPA que nunca actualiza el título. Cada página necesita su propio <title> de unos 50-60 caracteres, con el asunto de la página delante y la marca al final ("Precios — Acme"); en una SPA de React sin meta-framework eso significa fijar document.title en cada ruta.
Páginas: http://localhost:8099/, http://localhost:8099/app.html
4. 🟡 Impacto medio — Hay páginas sin meta description
Evidencia: 2 de 2 páginas no tienen <meta name="description">.
Impacto: Google rellena las dos líneas de debajo del enlace con el texto que rasque de la página, así que el argumento que lee un visitante antes de decidir si pulsa lo escribe un algoritmo en lugar del dueño.
Por qué importa: La description es el texto publicitario del resultado de búsqueda. No cambia el posicionamiento, cambia el porcentaje de clics. Una por página, de 150-160 caracteres, escrita para una persona que está decidiendo si pulsa — una genérica es peor que ninguna, porque parece escrita y así nadie escribe nunca la de verdad.
Páginas: http://localhost:8099/, http://localhost:8099/app.html
5. 🟡 Impacto medio — Hay páginas sin etiquetas Open Graph
Evidencia: A 2 de 2 páginas les falta og:title u og:image.
Impacto: Compartido por WhatsApp, LinkedIn, Slack o X, el enlace aparece como una URL pelada sin imagen ni título, y la gente pasa de largo. Cada vez que alguien comparte el sitio, se desperdicia.
Por qué importa: Las etiquetas Open Graph son lo que leen las redes sociales y las apps de mensajería para construir la tarjeta de vista previa. Sin ellas no hay tarjeta. El juego completo en el <head> de cada página es og:title, og:description, og:image, og:url y og:type, más twitter:card en summary_large_image — y og:image tiene que ser una imagen real de 1200x630, porque si falta se renderiza un recuadro en blanco en lugar de ninguna tarjeta.
Páginas: http://localhost:8099/, http://localhost:8099/app.html
Referencias: https://ogp.me/
6. 🟡 Impacto medio — Hay páginas que no declaran URL canónica
Evidencia: 2 de 2 páginas no tienen <link rel="canonical">.
Impacto: La misma página accesible con y sin www, con y sin barra final, o con un parámetro de campaña, cuenta como varias páginas que compiten entre sí, y reparte entre ellas la autoridad que el sitio se ha ganado.
Por qué importa: La etiqueta canonical nombra la única dirección real de una página para que los duplicados se consoliden en ella en vez de competir con ella. Cada página necesita su propio <link rel="canonical"> apuntando a su propia URL absoluta (http://localhost:8099/precios en la página de precios) — apuntar todas las páginas a la de inicio es el error habitual, y le dice a los buscadores que el resto del sitio no existe.
Páginas: http://localhost:8099/, http://localhost:8099/app.html
Referencias: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls
7. 🟡 Impacto medio — Sin datos estructurados (JSON-LD)
Evidencia: 2 de 2 páginas no contienen ningún bloque <script type="application/ld+json">.
Impacto: Los buscadores y los asistentes tienen que adivinar qué es el negocio, qué vende y dónde está. Los resultados enriquecidos (valoraciones, precios, FAQ, horarios) no están disponibles para un sitio que nunca los declara.
Por qué importa: Los datos estructurados son la versión legible por máquina de la página. Es como un asistente responde "quiénes son y qué hacen" con datos en vez de con suposiciones. Lo mínimo es un bloque application/ld+json en el <head> de la página de inicio declarando una Organization con el nombre real, url, logo, description y los enlaces sameAs a los perfiles — los valores tienen que ser ciertos, porque esto es el texto que un asistente repite tal cual.
Páginas: http://localhost:8099/, http://localhost:8099/app.html
Referencias: https://schema.org/Organization
8. 🟡 Impacto medio — Sin sitemap.xml
Evidencia: http://localhost:8099/sitemap.xml no se sirve.
Impacto: Las páginas que no están enlazadas desde la de inicio pueden pasar semanas sin que nadie las vea, o no indexarse nunca, y cada actualización tarda más en aparecer en las búsquedas.
Por qué importa: Un sitemap es la lista de páginas que el dueño quiere que se indexen. Sin él, los rastreadores solo encuentran lo que se van topando. Es un urlset de entradas <loc> en public/sitemap.xml (static/sitemap.xml en SvelteKit), con una línea Sitemap: http://localhost:8099/sitemap.xml en robots.txt que apunte a él — este escaneo alcanzó 2 páginas, listadas en "Páginas escaneadas".
Referencias: https://www.sitemaps.org/protocol.html
9. 🟡 Impacto medio — Sin llms.txt
Evidencia: http://localhost:8099/llms.txt no se sirve.
Impacto: Cuando a un asistente le preguntan por este negocio, tiene que reconstruir la oferta con el marcado que consiga interpretar. Un fichero corto y bien escrito es la diferencia entre que lo describan bien y que lo describan mal.
Por qué importa: llms.txt es el resumen en texto plano que una IA lee primero: qué es el sitio, para quién es y qué páginas importan. Vive en public/llms.txt — un H1 con el nombre, una cita con una frase sobre la oferta y el público, y una lista ## Pages con las páginas que merece la pena leer y una línea por cada una. Solo funciona si dice lo que el negocio vende de verdad; una versión de relleno es peor que ninguna, porque se lee como si fuera autoritativa.
Referencias: https://llmstxt.org/
10. 🟡 Impacto medio — La etiqueta <html> no declara idioma
Evidencia: 2 de 2 páginas no tienen el atributo lang en <html>.
Impacto: Los navegadores ofrecen traducir una página que ya está en el idioma del visitante, los lectores de pantalla eligen la voz equivocada y los buscadores pueden servir el sitio al país que no toca.
Por qué importa: Un atributo — lang en <html> — le dice a cualquier cliente en qué idioma está escrito el contenido. Las plantillas vienen con él vacío o copiado del starter, así que hay que ponerlo en el idioma en el que está el texto de verdad, no en el que traía la plantilla.
Páginas: http://localhost:8099/, http://localhost:8099/app.html
11. 🟡 Impacto medio — Hay imágenes sin texto alternativo
Evidencia: 1 de 1 imágenes no tienen atributo alt, repartidas en 1 de 2 páginas.
Impacto: Los visitantes ciegos oyen "imagen" en lugar del producto, la búsqueda de imágenes no devuelve nunca estas fotos y, en varias jurisdicciones, es una obligación de accesibilidad y no un detalle.
Por qué importa: El texto alt es lo que una imagen le dice a quien —o a lo que— no puede verla: lectores de pantalla, búsqueda de imágenes y cualquier rastreador que indexe la página. Tiene que describir lo que la foto muestra de verdad, en el idioma de la página — lo que significa mirar cada imagen, no generar texto a partir del nombre del fichero. Las imágenes puramente decorativas llevan un alt="" vacío, que no es lo mismo que no llevar ninguno.
Páginas: http://localhost:8099/
12. ⚪ Impacto bajo — Sin favicon
Evidencia: No se sirve ningún favicon ni se declara <link rel="icon"> en ninguna página rastreada.
Impacto: La pestaña, el marcador y la pantalla de inicio del móvil muestran una hoja en blanco. Es pequeño, es gratis de arreglar, y es la diferencia entre un negocio y una demo de fin de semana.
Por qué importa: El favicon es la única marca que le queda a un sitio en una fila de treinta pestañas abiertas. Necesita un public/favicon.svg más un public/apple-touch-icon.png de 180x180 para la pantalla de inicio del móvil, ambos referenciados con <link rel="icon"> y <link rel="apple-touch-icon"> en el <head>.
Alcance verificado
Este análisis automatizado cubre lo que se puede verificar desde fuera. Lo que exige acceso al servidor (autorización, validación de entradas, límites de tasa, copias de seguridad) corresponde a la parte manual de la auditoría.
Verificado automáticamente
- Cabeceras de seguridad HTTP (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- Redirección HTTP→HTTPS (si el sitio responde en http:// plano)
- Secretos y credenciales expuestos en 1 bundle(s) de JavaScript
- Source maps expuestos (código fuente original descargable desde la URL)
- Calidad del sitio y visibilidad ante las IA en 2 página(s) — metadatos, encabezados, datos estructurados, robots.txt/llms.txt/sitemap, página 404, recursos rotos, peso del JS
Los errores de consola del navegador no se inspeccionaron: Playwright no está instalado. Instálalo (npm i -D playwright && npx playwright install chromium) y vuelve a ejecutar para incluir los errores en tiempo de ejecución.
Siguientes pasos recomendados
1. Atiende los hallazgos por orden de severidad. 2. Complétalo con una revisión manual del lado del servidor.
Para la sección del sitio web, npx vibeward@latest init instala una skill que lee estos hallazgos, aplica los arreglos que puede y vuelve a escanear para verificarlo.
Generado con vibeward v0.6.0 — análisis automatizado y no destructivo realizado desde fuera.