# Autoevaluación de seguridad

Evaluación propia de la aplicación web contra **OWASP ASVS 4.0.3, nivel 1**.
La hacemos nosotros: **no es una auditoría** y nadie externo la ha revisado.
Se publica para que cualquiera pueda contrastarla con el código.

Última revisión: 2026-09-17 · Alcance: el sitio estático y el motor que corre en
el navegador. La CLI y el servidor MCP corren en la máquina del usuario, con sus
permisos, y quedan fuera de este alcance.

Leyenda: **Cumple** · **Parcial** (con la limitación explicada) · **No aplica**
(el mecanismo no existe en esta arquitectura).

---

## V1 — Arquitectura y diseño

| | Estado | Nota |
|---|---|---|
| Componentes identificados y con función acotada | Cumple | Tres paquetes: `core` (motor), `mcp`, `cli`, y la app web. El motor no conoce la red. |
| Confianza en el cliente documentada | Cumple | Todo ocurre en el cliente **a propósito**: es la propuesta de valor, no un descuido. El servidor solo entrega archivos estáticos. |
| Segregación de entornos | Cumple | Vercel separa producción de las vistas previas por rama. |

## V2 / V3 — Autenticación y sesiones

**No aplica.** No hay cuentas, contraseñas, tokens, sesiones ni cookies de
sesión. Un mecanismo que no existe no se puede romper. Si algún día añadimos
cuentas, esta sección deja de poder decir esto.

## V4 — Control de acceso

**No aplica** en el sentido clásico: no hay datos de un usuario a los que otro
pueda llegar, porque no hay almacenamiento compartido ni servidor con estado.

## V5 — Validación de entrada

| | Estado | Nota |
|---|---|---|
| Toda entrada se trata como no confiable | Cumple | Cada PDF pasa por `classifyError`: un archivo dañado, cifrado o que no es PDF produce un error con mensaje, nunca un fallo silencioso ni un cuelgue. |
| Protección contra entradas que agoten recursos | Cumple | El diff tiene un límite (`maxCells`) y degrada en vez de colgar el navegador con dos documentos enormes y sin relación. |
| Salida escapada | Cumple | React escapa por defecto. Hay seis usos de `dangerouslySetInnerHTML`: cinco insertan JSON-LD generado por nosotros, y uno —el resaltado de marcadores en PDF a Markdown— sí recibe texto del documento del usuario. Ese pasa por `highlightPlaceholders`, que escapa **primero** y reconoce el patrón `[NOMBRE_1]` **después**; el patrón solo admite mayúsculas, guiones bajos y dígitos, así que no puede componer una etiqueta. Comprobado por `test/highlight.test.ts`, incluido un PDF con `<script>` dentro. |

## V6 — Criptografía almacenada

**No aplica.** No almacenamos nada, así que no hay nada que cifrar en reposo.
No implementamos criptografía propia en ningún punto.

## V7 — Errores y registro

| | Estado | Nota |
|---|---|---|
| Los errores no filtran datos sensibles | Cumple | Los mensajes describen la causa («el PDF está protegido con contraseña»), nunca el contenido. |
| No se registran datos personales | Cumple | No hay registro de servidor con contenido: el contenido no llega al servidor. |

## V8 — Protección de datos

| | Estado | Nota |
|---|---|---|
| Datos sensibles no enviados a terceros | Cumple | `connect-src 'self'` en la CSP. Comprobado por `test/no-network.test.ts`. |
| Minimización | Cumple | No recogemos el archivo, ni su nombre, ni su tamaño, ni su número de páginas. |
| Enmascaramiento antes de exponer | Cumple | La anonimización se aplica por defecto en PDF a Markdown. |

## V9 — Comunicaciones

| | Estado | Nota |
|---|---|---|
| TLS en todo el sitio | Cumple | HTTPS con HSTS `max-age=63072000; includeSubDomains; preload`, fijado en `vercel.json`. |
| Sin recursos de terceros | Cumple | Todo se sirve desde el propio dominio, incluidos los ~11 MB del modelo de OCR. Comprobado por prueba automática. |

## V10 — Código malicioso

| | Estado | Nota |
|---|---|---|
| Dependencias fijadas | **Parcial** | La mayoría se declaran con versión exacta. Cuatro usan rango con cursor: `tesseract.js`, `zod` y `@modelcontextprotocol/sdk` en los dos paquetes. El `package-lock.json` fija la versión que se instala de verdad, así que una compilación es reproducible, pero los rangos deberían cerrarse. |
| Sin carga de código remoto | Cumple | `script-src 'self'`: el navegador rechaza cualquier script de otro origen. |

## V12 — Archivos y recursos

| | Estado | Nota |
|---|---|---|
| Los archivos subidos no se ejecutan | Cumple | No hay subida. El archivo se lee como `ArrayBuffer` en un Web Worker. |
| Tipo de archivo validado | Cumple | Se valida la estructura real del PDF, no la extensión. |

## V14 — Configuración

| | Estado | Nota |
|---|---|---|
| Cabeceras de seguridad | Cumple | CSP, HSTS, `X-Content-Type-Options`, `Referrer-Policy: no-referrer`, `Permissions-Policy`, `Cross-Origin-Opener-Policy`, `frame-ancestors 'none'`, `base-uri 'self'`, `form-action 'self'`. Comprobadas por prueba automática. |
| CSP sin `unsafe-inline` | **Parcial** | La CSP incluye `'unsafe-inline'` en `script-src` y `style-src`. Next.js inserta su script de arranque en línea, y la alternativa correcta —un *nonce* por respuesta— exige un servidor que genere ese nonce en cada petición. Con exportación estática no hay tal servidor. Es una debilidad real y la dejamos escrita en vez de esconderla; el impacto está acotado porque `connect-src 'self'` impide la exfiltración aunque se lograra inyectar código. |
| `wasm-unsafe-eval` | Cumple, con motivo | Necesario para el OCR: WebAssembly no corre sin él. Es el permiso mínimo para eso, no `unsafe-eval` completo. |
| Canal de reporte de fallos | Cumple | `/.well-known/security.txt` (RFC 9116). |

---

## Pendiente

- Repetir la evaluación contra ASVS 5.0 y anotar las diferencias.
- Quitar `'unsafe-inline'` de `style-src`, que es más fácil que el de scripts.
- Cerrar los cuatro rangos con cursor a versión exacta.
- Una revisión externa. Mientras no la haya, este documento es lo que es: lo que
  decimos nosotros de nosotros mismos, con el código delante para contrastarlo.
