Seguridad
Nuestro modelo de seguridad en una frase: no hay nada en un servidor que atacar. Todo lo que aparece a continuación es verificable desde tus DevTools.
Arquitectura
El generador es una aplicación de página única estática servida desde Cloudflare Pages: sin cuentas de usuario, sin autenticación y sin ningún código de backend en el flujo de generación. Cada operación de generación, codificación, escaneo y renderizado de QR se ejecuta completamente dentro de tu navegador. Un pequeño endpoint sin servidor lo acompaña y se enumera aquí en lugar de ocultarlo: /api/contact (el formulario de contacto, que guarda lo que envías en Cloudflare D1 y nos lo envía por correo). No se llama mientras generas un código QR; el comprobador de seguridad de URL es un producto aparte en check.qr.abundera.ai. Comprobar si un código QR es seguro
Modelo de amenaza
Dado que el generador no recopila, almacena ni transmite datos de usuario (los dos endpoints anteriores solo gestionan lo que envías explícitamente), las amenazas más comunes en aplicaciones web — robo de credenciales, brecha de base de datos, secuestro de sesión, inyección del lado del servidor — no se aplican a él. La superficie de ataque restante es el paquete de activos estáticos (HTML, CSS, JavaScript) servido desde nuestro origen. Diseñamos asumiendo:
- Compromiso del origen: un atacante sustituye uno de nuestros activos empaquetados por uno malicioso. El CSP que se describe a continuación limita el radio de explosión; los tokens de cache-buster facilitan la reversión a una versión conocida como válida.
- Entrada suministrada por el usuario: payloads de QR, nombres de vCard, contraseñas WiFi, filas de CSV por lotes, contenidos de imágenes escaneadas. Cada ruta de entrada se trata como no confiable cuando se renderiza de vuelta al DOM.
- Imágenes suministradas por el usuario: URLs de fotos de vCard y cargas de logotipos. Las imágenes se renderizan en un canvas, nunca se incrustan en el DOM como marcado sin procesar.
- Extensiones de navegador de terceros: fuera del alcance. Si una extensión en tu navegador tiene permiso para modificar todas las páginas, puede modificar la nuestra. Nuestras garantías se mantienen cuando la página se carga sin modificaciones.
Política de Seguridad de Contenido — por directiva
La política que aparece a continuación se lee del archivo _headers desplegado en el momento de compilar esta página, así que no puede desincronizarse de lo que envía el edge. Verifícala en los encabezados de respuesta de cualquier solicitud.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://abundera.ai 'wasm-unsafe-eval' https://challenges.cloudflare.com;
worker-src 'self' blob:;
style-src 'self' 'unsafe-inline' https://abundera.ai;
font-src 'self';
img-src 'self' data: blob: https:;
connect-src 'self' https: https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'Qué permite cada directiva y dónde implica concesiones:
default-src 'self'— el piso duro. Todo lo que no relajemos explícitamente permanece en el mismo origen.script-src 'self' https://abundera.ai 'wasm-unsafe-eval' https://challenges.cloudflare.com— sin<script>en línea, sineval().abundera.aies nuestro propio dominio padre y sirve un único script compartido, el selector de idioma.challenges.cloudflare.comes el widget de Turnstile usado únicamente en /contact/.'wasm-unsafe-eval'es necesario por la ruta de compilación WebAssembly de nuestra librería de codificación QR; NO permite el uso tradicional deeval(). Nuestro propio control pre-despliegue escanea cada página HTML en busca de scripts en línea y falla la compilación.style-src 'self' 'unsafe-inline'— una concesión real. Nuestra vista previa de QR calcula colores a nivel de píxel en línea (atributos de estilo en módulos individuales). Una lista de permitidos basada en hash funcionaría, pero fallaría en cualquier actualización de estilo sin un despliegue. Compromiso: aceptamos la política de estilos ligeramente más débil; los estilos no pueden filtrar datos de todas formas (CSS no tiene alcance deconnect-src).img-src 'self' data: blob: https:—data:para renderizados de QR en línea,blob:para URLs de descarga de exportación,https:para URLs de fotos y avatares de vCard suministrados por el usuario. Las URLs suministradas por el usuario nunca se ejecutan, solo se renderizan. Ten en cuenta que esta directiva también es un canal de salida: cualquier script que se ejecute en la página podría solicitar una imagen de cualquier host HTTPS y colocar datos en la URL. Aceptamos eso porque la función lo necesita, y lo decimos aquí en lugar de describir la política como infranqueable.connect-src 'self' https: https://challenges.cloudflare.com— la que hay que leer con cuidado.https:es un origen de esquema: permite quefetch(), XMLHttpRequest, WebSocket sobre TLS, EventSource ysendBeacon()alcancen cualquier origen HTTPS, no solo los que escribiste. Está ahí para que el campo de foto de vCard y la comprobación de seguridad de URL puedan obtener una dirección que pegaste. No evita la filtración por un script malicioso; ninguna directiva CSP lo hace una vez que el script ya se ejecuta como nosotros. Lo que evita la filtración es que nuestro código publicado no hace esas solicitudes, algo que puedes comprobar en la pestaña Network.frame-src 'self' https://challenges.cloudflare.com— solo el widget de Turnstile puede enmarcarse.frame-ancestors 'none'— ningún otro sitio puede incrustarnos. Previene el clickjacking.base-uri 'self'— una etiqueta<base>inyectada de forma maliciosa no puede redirigir las URLs relativas al origen de un atacante.form-action 'self'— cualquier formulario inyectado solo puede enviar datos de vuelta a nuestro origen. El formulario de contacto es el único formulario que envía datos a algún sitio.
Lo que CSP no puede hacer
CSP limita desde dónde cargará código el navegador. No verifica qué hace el código cargado. Si nuestro origen se viera comprometido, el primer punto del modelo de amenaza, el script del atacante se ejecutaría con los mismos permisos que el nuestro, incluidas las concesiones de connect-src e img-src anteriores. Nuestras defensas frente a eso están por delante de CSP: los despliegues salen a través de una única cuenta de Cloudflare con tokens con alcance limitado, los nombres de archivo de los activos llevan un hash de contenido para que un archivo sustituido cambie cada URL que lo referencia, y el service worker solo almacena en caché respuestas GET del mismo origen. El manifiesto describe el sitio tal como se publica. No es una afirmación de que ninguna página web pueda enviar datos a ningún sitio jamás, y preferimos que nos exijas cumplir la declaración más estrecha y verificable.
Se aplican diferentes políticas CSP a /bio/* (relajación de img-src para avatares suministrados por el usuario) y /embed/* (relajación de frame-ancestors para incrustación intencional). Ambas están documentadas en site/_headers.
Encabezados de transporte y marcos
- HSTS:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload— 1 año, todos los subdominios, elegible para la lista de precarga HSTS. Los navegadores conformes rechazan las degradaciones a HTTP. - X-Frame-Options: DENY — redundante con CSP
frame-ancestors, mantenido para compatibilidad con navegadores más antiguos. - X-Content-Type-Options: nosniff — previene ataques de confusión de MIME.
- Referrer-Policy: strict-origin-when-cross-origin — los clics en enlaces salientes filtran el origen pero no la ruta.
- Permissions-Policy:
camera=(self), microphone=(), geolocation=()— cámara permitida solo para nuestro propio escáner; micrófono y ubicación explícitamente denegados incluso si se incrusta.
Service worker
Nuestro service worker (site/sw.js) almacena en caché solo activos del mismo origen. El controlador de fetch rechaza explícitamente las solicitudes de origen cruzado y los métodos que no son GET — puedes leer la lógica en /sw.js (el código se publica sin minificar lo suficiente como para seguirlo). Las escrituras en caché están envueltas en event.waitUntil() para que no puedan descartarse en medio de una navegación.
Sanitización de entradas
Cada ruta de renderizado que acepta entrada de usuario la trata como texto no confiable:
- Las vistas previas de payload de QR usan
textContent, noinnerHTML. - Los destinos de compartición (portapapeles,
navigator.share()) pasan el texto del usuario como cadena, nunca como marcado. - Las exportaciones SVG se generan desde nuestra librería de codificación; el contenido del usuario se codifica en base64 en
<image>xlink:href, no se inyecta como elementos SVG. - Las vistas previas de impresión usan una URL blob, no
document.write(). - El análisis de localStorage está envuelto en
try/catch— una entrada corrupta produce un valor predeterminado vacío nuevo, nunca una excepción que pueda desenrollarse en una ruta de código activa. - Las URLs suministradas por el usuario que se muestran como botones "Abrir enlace" están restringidas a
http(s)://— los esquemasjavascript:ydata:son rechazados.
Obtención de imágenes de origen cruzado
Cuando un usuario pega una URL https: como foto de vCard o logotipo, el navegador la obtiene sujeto a CORS y la lista de permitidos img-src de nuestro CSP. La imagen se renderiza en un canvas. Nunca se convierte en DOM activo, nunca se ejecuta como código y nunca llega a nuestro origen — la obtención es navegador → imagen remota, y el resultado se pinta en el lado del cliente. Un atacante que controla una URL de imagen remota puede rastrear que la URL fue cargada (una línea de registro en su propio servidor), pero no puede filtrar nada de nuestra página.
Integridad de Subrecursos (SRI)
Todo el JavaScript y CSS que enviamos es del mismo origen. No cargamos scripts ni hojas de estilo de terceros, por lo que los hashes SRI no son aplicables. Si alguna vez cargamos un activo de terceros, incluiremos un atributo integrity SRI en él y documentaremos el proceso de actualización de hash en esta página.
Reportar una vulnerabilidad
Si descubres un problema de seguridad que afecta a Abundera QR — ya sea en nuestro código, nuestro despliegue o en una dependencia que enviamos — por favor repórtalo de forma privada a security@abundera.ai. Nuestro objetivo es triagiar en 72 horas. También puedes contactarnos a través de los detalles de contacto en nuestro archivo /.well-known/security.txt.
Sin programa de recompensas (por ahora)
Actualmente no ofrecemos recompensas pagadas, pero cada informe válido confirmado recibe crédito en el registro de cambios y nuestro agradecimiento público.
Verifica cualquiera de lo anterior
Cada afirmación en esta página es verificable desde los DevTools de tu navegador sin necesidad de confiarnos:
- CSP: DevTools → Red →
/→ Encabezados. Lee el encabezado de respuestaContent-Security-Policy. - HSTS + encabezados de seguridad: mismo lugar — todos
Strict-Transport-Security,X-Frame-Options, etc. son visibles. - Sin llamadas salientes: DevTools → Red → Fetch/XHR. Genera un QR. Observa cómo el contador se mantiene en cero.
- Alcance del service worker: DevTools → Aplicación → Service Workers. Verifica la fuente del script y la lista de activos en caché.
- Sin cookies: DevTools → Aplicación → Cookies. Vacío.
- Guía completa: la sección de verificación del manifiesto.
Contacto
Divulgaciones de seguridad: security@abundera.ai