vibeward
Informe de ejemplo

Lo que imprime de verdad

Este es un informe completo, sin editar. Salió de escanear una app rota a propósito servida en localhost — no el sitio en vivo de nadie, y por eso la dirección que aparece dentro lo dice. Todos los escáneres de la competencia te enseñan la captura de un panel; esto es el entregable que le pasarías a un cliente.

Dos cosas que conviene leer despacio

El token bearer hardcodeado se detecta por su uso, no por su forma. Tras minificar, la variable es una sola letra y el valor no tiene prefijo de proveedor, así que no hay patrón que buscar: lo que lo delata es un literal que acaba detrás de Bearer. Un token que el cliente obtiene al hacer login es un identificador sin literal, y se queda callado.

La clave publishable de Supabase que está tres líneas más arriba no se reporta en absoluto. Va en el cliente por diseño, y un escáner que la marca te está enseñando a ignorar al escáner. Seis de ocho hallazgos sobre un corpus real eran exactamente ese tipo de ruido.

La sección de web se puntúa en su propia escala. Nunca afecta al veredicto de seguridad, ni al archivo SARIF, ni al código de salida — un favicon que falta no es un hallazgo al lado de una credencial filtrada.

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.
SeveridadHallazgos
🔴 Crítico0
🟠 Alto1
🟡 Medio2
⚪ Bajo4

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ónEstadoImpactoNota
robots.txt deja entrar a los rastreadores de IA✅ ok
Rastreadores de IA bloqueados como se declaró⚪ no aplicano 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 evaluadano 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

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.

La app que lo genera

Cuatro archivos, rotos a propósito. Nada de esto es una credencial real: la clave de Supabase es un valor sintácticamente válido que no apunta a ningún sitio, y el token bearer está escrito como placeholder — pon ahí 48 caracteres hex cualesquiera y te sale el informe de arriba. El placeholder no es remilgo, es la herramienta funcionando. Con el valor real impreso aquí, esta misma página se escaneaba como token bearer hardcodeado en código cliente, porque un literal que llega a una cabecera Authorization es justo lo que busca la regla y no puede distinguir una demo de un despliegue. Un placeholder no tiene forma de credencial, así que se queda callado — y los hallazgos de seguridad no se pueden suprimir por configuración, por diseño.

python3 -m http.server 8099
npx vibeward@latest http://localhost:8099/ --yes --lang es

index.html

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Nimbus Invoices</title>
</head>
<body>
<h1>Nimbus Invoices</h1>
<p>Send invoices in seconds. Built in a weekend.</p>
<a href="app.html">Open the app</a>
<img src="logo.png">
<script src="assets/app.js"></script>
</body>
</html>

assets/app.js

//# sourceMappingURL=app.js.map
var SUPABASE_URL = "https://abcdefghijklmnop.supabase.co";
var SUPABASE_ANON_KEY = "sb_publishable_9f2c1d4e7a8b3c5d6e0f1a2b";
var B = "PUT_ANY_48_HEX_CHARACTERS_HERE";
function api(p) {
  return fetch(SUPABASE_URL + p, {
    headers: { apikey: SUPABASE_ANON_KEY, Authorization: "Bearer ".concat(B) }
  });
}
window.api = api;