# Auditoría integral · 2026-07-30

Todo verificado **en caliente** contra la base viva, los dominios en
producción y los servicios locales corriendo. Nada de este documento sale de
leer código sin comprobarlo.

**Alcance:** seguridad de la base, superficie pública (pruebas de penetración
reales), la Estación Fiscal, trazabilidad, consistencia de datos, calidad de
código y dependencias.

---

## RESUMEN

| Severidad | Cantidad | Estado |
|---|---|---|
| 🔴 Alta | 2 | ✅ las dos cerradas |
| 🟡 Media | 2 | ✅ una cerrada, una cerrada a medias con motivo |
| 🔵 Baja | 3 | ✅ una cerrada, **dos eran falsos positivos míos** |

**Lo importante:** no se encontró ninguna fuga de datos, ningún secreto
expuesto, ninguna puerta abierta ni ningún dato corrupto. Los dos hallazgos
altos son de **disponibilidad** (tumbar el servicio), no de robo de
información.

## ESTADO AL CIERRE (todo arreglado el mismo día)

| # | Hallazgo | Estado |
|---|---|---|
| 1 | Cualquiera podía llenar la base | ✅ **Cerrado de raíz**: la tabla no acepta escritura de nadie; se entra por una puerta que valida, recorta y frena |
| 2 | El POS sin freno de fuerza bruta | ✅ **Cerrado**: 5 intentos con espera creciente, el PIN bueno tampoco pasa mientras dure el castigo (v2.8.0) |
| 3 | Datos fiscales del negocio públicos | ✅ **Cerrado**: permisos columna por columna; pedir el RIF da permiso denegado |
| 4 | Cinco vulnerabilidades de dependencias | ✅ **Crítica y alta cerradas** (jspdf 4.2.1, PDF probado). La de react-router queda abierta con motivo: no hay arreglo en la línea 6 y **el vector no aplica** (se comprobó que la app no navega a donde diga la URL) |
| 5 | Índices duplicados | ⚠️ **Falso positivo mío**: el índice DESC tiene 75 escaneos contra 19 del otro, o sea que se usa MÁS. Mi detección no consideró que el orden los hace distintos y útiles |
| 6 | Dos llaves foráneas sin índice | ✅ **Cerrado** |
| 7 | El libro fiscal legible por cualquiera | ⚠️ **Mal medido**: los permisos reales de Windows solo dan acceso a SYSTEM, Administradores, el usuario, y un SID huérfano de una cuenta borrada. El `-rw-r--r--` que reporté es una traducción falsa de git bash |

---

## 🔴 HALLAZGO 1 · Cualquiera puede llenar la base y tumbar todo

**Dónde:** tabla `public.eventos_web`, política `eventos_web_insert_publico`.

**Qué encontré, verificado:**
- La política deja INSERT a `anon` con `with_check: true` (sin ninguna
  condición).
- La llave `anon` es pública por diseño: va dentro del bundle que descarga
  cualquier navegador.
- **No tiene freno**: existe `fn_rate_limit` en la base, pero esta tabla no lo
  usa (la política es un INSERT directo).
- **No se limpia nunca**: se comprobó que el cron `limpiar-datos-viejos` NO
  toca esta tabla (`prosrc ~ 'eventos_web'` → falso).
- Hoy: 145 filas, 88 kB, la más vieja del 20-jul.

**El daño:** un script cualquiera puede insertar millones de filas hasta
llenar los 500 MB del plan free. Cuando la base se llena, **deja de funcionar
todo al mismo tiempo**: la tienda, el panel, las comandas y la facturación de
los clientes de la imprenta.

**Cómo se arregla:** freno por dirección en el insert (o mover la medición a
una función `SECURITY DEFINER` que use `fn_rate_limit`), más limpieza
automática de los eventos con más de 90 días en el cron que ya existe.

---

## 🔴 HALLAZGO 2 · El punto de venta de las tablets no tiene freno

**Dónde:** `apps/cerebrito/src/pos.ts`.

**Qué encontré, verificado:**
- El POS escucha en **`0.0.0.0:8738`**: toda la red del local, no solo la
  laptop (es a propósito, las tablets tienen que entrar).
- Se protege con un **PIN de 4 a 8 dígitos** (`/^\d{4,8}$/`).
- **No hay freno anti fuerza bruta**: se buscaron las palabras
  `fallidos|intentos|freno|castigo|espera` en todo el archivo → **cero
  coincidencias**.

**El daño:** cualquiera conectado al wifi del restaurante (un cliente
sentado, el local de al lado, quien tenga la clave del wifi) puede probar los
10.000 PIN de cuatro dígitos a velocidad de red local: cuestión de segundos.
Con el PIN adentro puede tomar pedidos, abrir mesas, cobrar y ver los totales
del negocio.

**Comparación que lo pone en contexto:** el panel de la imprenta SÍ tiene
freno (se le puso el 29-jul) y la Estación Fiscal SÍ tiene freno (5 intentos,
15 minutos). El POS, que es el único expuesto a una red compartida, es el que
no lo tiene.

**Cómo se arregla:** freno por dirección (5 intentos, espera creciente) y
subir el mínimo del PIN a 6 dígitos para instalaciones nuevas.

---

## 🟡 HALLAZGO 3 · Los datos fiscales del negocio quedan públicos

**Dónde:** política `restaurantes` de lectura pública.

**Qué encontré, verificado:** consultando con la llave `anon` (la del
navegador), un anónimo recibe **42 columnas** de cada restaurante, entre ellas
`rif`, `razon_social` y `domicilio_fiscal`.

Hoy vienen en `null` porque ningún negocio ha cargado sus datos fiscales, pero
**el día que los carguen quedan expuestos**, y la tienda del cliente no los
necesita para funcionar.

**El daño:** permite raspar los datos fiscales de todos los negocios de la
plataforma. No es una fuga de datos de clientes finales, pero es exposición
innecesaria de información de tus clientes.

**Cómo se arregla:** que la lectura pública devuelva solo las columnas que la
tienda usa (vista pública o política por columnas).

---

## 🟡 HALLAZGO 4 · Cinco vulnerabilidades en dependencias

Verificado con `npm audit --omit=dev`:

| Severidad | Paquete | Problema | Dónde se usa |
|---|---|---|---|
| CRÍTICA | `jspdf` | Denegación de servicio por expresión regular | `apps/restaurante/src/lib/reporte.ts` (PDF del dueño) |
| ALTA | `jspdf-autotable` | (arrastra la anterior) | igual |
| MEDIA | `dompurify` | XSS (viene dentro de jspdf) | igual |
| MEDIA | `react-router` | Redirección abierta que lleva a XSS | **`apps/cliente`: la tienda pública** |
| MEDIA | `react-router-dom` | igual | igual |

**Matiz que cambia la prioridad:** aunque la crítica sea `jspdf`, esa librería
solo la usa el panel del dueño con datos de su propio negocio, así que un
atacante no controla la entrada. En cambio `react-router` está en la app
**pública** del cliente: ahí un atacante sí puede mandar un enlace preparado.
**La moderada de react-router es más urgente que la crítica de jspdf**, y
además se arregla sin romper nada (`npm audit fix`); jspdf exige subir de
versión mayor y probar los reportes.

---

## 🔵 HALLAZGOS MENORES

**5 · Índices duplicados** en `fiscal.respaldo` (dos índices iguales). Peso
muerto en cada escritura del respaldo.

**6 · Dos llaves foráneas sin índice**: `recetas_productos.ingrediente_id` y
`compras_ingredientes.ingrediente_id`. Hoy son tablas chicas; con volumen,
borrar un ingrediente se vuelve lento.

**7 · El libro fiscal es legible por cualquier usuario del equipo**
(`libro.jsonl` con permisos `-rw-r--r--`). Quien se siente en esa computadora
puede leerlo, aunque no modificarlo sin romper la cadena de hashes. El
respaldo que sale del equipo sí va cifrado.

---

## ✅ LO QUE SE VERIFICÓ Y ESTÁ IMPECABLE

Esto también es resultado de la auditoría, y es la mayor parte:

**Base de datos**
- **Cero** tablas públicas sin RLS.
- **Cero** funciones `SECURITY DEFINER` sin `search_path` fijo (el vector
  clásico de secuestro de esquema).
- El esquema `fiscal` **no está expuesto** a `anon` ni a `authenticated`
  (cero permisos).
- Las tablas sensibles (`pedidos`, `clientes`, `perfiles`, `pagos`,
  `pagos_mesa`, `cuentas_mesa`, `repartidores`) devuelven **0 filas** a un
  anónimo. Probado con la llave pública real.

**Superficie pública (pruebas de penetración en vivo)**
- Panel de la imprenta: `clientes`, `numeracion` y `guardar_imprenta` sin
  sesión → **401**.
- Emisión de documentos y halado de ventas sin llave → **401**.
- Inyección SQL en la verificación pública (`' OR '1'='1` y
  `'; DROP TABLE...`) → rebotada por el verificador antes de tocar la base.
- Token de QR inventado → **404**, sin filtrar nada.
- **Cero secretos** en el código que llega al navegador (se buscaron JWT,
  `sb_secret_`, `sbp_`, `glpat-`).

**Estación Fiscal**
- Las siete pantallas sin sesión → redirigen a entrar.
- Escribir en el libro sin llave de máquina → **401**. Anular sin sesión →
  **401**. Puerta del fiscal con clave inventada → rechazada.
- **La cadena de hashes del libro vivo está íntegra**, verificada asiento por
  asiento, y los correlativos no tienen saltos ni repetidos.

**Trazabilidad (artículo 3)**
- Se intentó **borrar** el rastro del panel → *"El rastro del panel solo
  agrega: no se edita ni se borra"*.
- Se intentó **editar** el rastro → mismo bloqueo.
- Se intentó **falsificar** una línea del respaldo cifrado → *"El respaldo
  solo agrega: una línea guardada no se modifica"*.

**Consistencia de datos: cero incoherencias**
- Pedidos sin restaurante, renglones sin pedido, entregados sin fecha,
  totales negativos, cuentas fantasma: **0** en todos.
- Pagos sin cuenta, pagos sin pedido, mesas abiertas hace días, entregados en
  cero: **0** en todos.
- Cola fiscal sin atascos.

**Código**
- `typecheck` limpio en los cuatro workspaces (restaurante, cliente, fiscal,
  cerebrito).
- `lint` limpio.
- **Cero** funciones o cron jobs rotos por renames de columnas.
- **Cero** triggers apuntando a funciones inexistentes.

---

## Orden sugerido para arreglarlo

1. **Hallazgo 1** (llenar la base): es el que puede tumbar todo y lo puede
   hacer cualquiera con un script.
2. **Hallazgo 2** (PIN sin freno): superficie de red compartida.
3. **Hallazgo 4, solo react-router**: `npm audit fix`, sin romper nada.
4. **Hallazgo 3** (columnas fiscales públicas): antes de que un negocio cargue
   su RIF.
5. **Hallazgo 4, jspdf**: versión mayor, hay que probar los reportes.
6. **Menores 5, 6 y 7.**
