Ir al contenido

Blog ·

Un escaneo que cerró su propia franja

Cada escaneo programado con éxito marcaba su propia franja como vencida, por una palabra de la enumeración equivocada. Dos defectos que solo se ven midiendo.

Por Provibr

Nuestro escaneo de exposición tiene un planificador. Reclama una franja, envía un comando de escaneo al agente que alcanza la máquina y espera dos cosas: la medición y la finalización del comando que la transportó. Cuando una franja termina sin respuesta se marca como vencida, y la máquina aparece como «sin medición reciente».

Cada escaneo con éxito marcaba su propia franja como vencida. Cuatro de cuatro, medidos contra un agente real, mientras las cuatro mediciones se leían y guardaban sin problema. El panel informaba de que cuatro máquinas no se habían escaneado hace poco, minutos después de escanearlas.

Una palabra, de otra tabla

El manejador que cierra una franja sin respuesta empezaba con una salida temprana: si el comando tuvo éxito, no hay nada que cerrar, así que se sale.

La palabra con la que comparaba era succeeded. Esa palabra no existe en la enumeración de estados de comando, que reza pending, sent, completed, failed, timeout y cancelled. Es la palabra de otra tabla por completo, la que registra las instalaciones de agentes, y llegó aquí porque alguien escribió el manejador teniendo delante los estados de esa otra tabla y porque el parámetro estaba tipado como texto llano.

El texto llano es toda la historia. La comparación compila contra cualquier texto, así que nada objeta, y el fallo no es una excepción sino una rama que nunca se toma. El manejador recorrió su camino de «no llegó respuesta» contra cada escaneo con éxito y marcó obedientemente cada franja como vencida.

Ninguna prueba unitaria podía encontrarlo. En una imitación no hay ciclo de vida de comando: la imitación es lo que entrega el estado, así que el estado es lo que escribió quien redactó la prueba, y esa persona escribió la misma palabra equivocada. Solo un agente real, enviando finalizaciones reales por el distribuidor real, produce el estado que lo expone. El parámetro está ahora tipado a los tres estados que pueden llegar de verdad a esta función, tal como el manejador equivalente de monitorización lo estuvo siempre. Dos funciones, la misma pregunta, y solo una escrita con cuidado.

Y los dos mensajes llegan en desorden

Arreglar la comparación no bastaba, y la razón es una segunda cosa que solo enseña un agente real.

El agente envía dos mensajes separados: el resultado del escaneo y la finalización del comando. Viajan por su cuenta. Medido, el resultado llegó primero, catorce milisegundos antes que la finalización. Ese intervalo es lo bastante pequeño para que nadie piense en él y lo bastante grande para que ocurra siempre.

Cuando el manejador de finalización corre, la franja suele estar ya cerrada por el resultado. Una actualización sin condición reabre entonces una franja terminada y la marca vencida otra vez, que es el fallo original con otro sombrero. Por eso la actualización queda sujeta a los dos estados en los que una franja aún espera respuesta. Si ya está resuelta, el manejador no cambia nada.

Es una forma general: cuando un hecho puede llegar por dos caminos, el escritor que no es el principal debe declarar de qué estados le está permitido salir.

Un hallazgo cerrado que detuvo toda la flota

El segundo defecto es más callado y causó más daño.

Cuando una fuente de vulnerabilidades se actualiza, los hallazgos abiertos se reevalúan contra los datos nuevos. Un hallazgo cuya coincidencia desapareció se cierra, y se cierra como no verificado y no como resuelto, porque no se midió nada: lo que cambió es nuestra inferencia, no la máquina. Esa distinción es la razón de que ese estado exista.

La reevaluación recuperaba los hallazgos derivados existentes para distinguir «actualizar este» de «crear uno nuevo». Recuperaba solo los abiertos. Es un filtro de aspecto razonable y es incorrecto, porque la restricción de unicidad de debajo no sabe nada de estados: está sobre el servidor y una huella del hallazgo, y punto.

La secuencia va así: una base nacional de vulnerabilidades corrige un rango de versiones, nuestra coincidencia desaparece, el hallazgo se cierra. Semanas después esa misma base revisa el rango a la inversa, cosa que ocurre de verdad, y la coincidencia vuelve. La consulta no ve la fila cerrada. El código toma la rama «esto todavía no existe». La inserción choca con el índice único y lanza una violación de restricción.

Y entonces esa violación escapa. Quien llama la recoge como «la ronda de avisos falló», y esa ronda no reevalúa absolutamente nada, para todos los clientes de la plataforma. La condición que la causó no desaparece sola, así que cada ronda posterior falló igual. Un hallazgo que se había cerrado y reabierto congeló la reevaluación de una flota entera, de forma permanente, con una línea de registro como único síntoma.

El arreglo es pequeño: incluir las filas cerradas y, cuando un hallazgo cerrado vuelve a coincidir, reabrirlo con su cronología intacta en lugar de empezar uno nuevo. Un hallazgo reabierto es un hecho distinto de uno nuevo, y quien lo clasifica merece ver que esto volvió y no que llegó.

Un ciclo de fuentes, medido tres veces

El proceso de fuentes se escribió antes de que hubiera manera de ejecutarlo sin tocar una base de datos real, y esa es la trampa en la que cayó la ronda de seguridad anterior: importar el paquete de base de datos lee el entorno y conecta, de modo que una prueba unitaria con una función de descarga inyectada escribió en silencio cuatro avisos en una base de desarrollo mientras informaba en verde.

El lado de almacenamiento es ahora una costura, y el ciclo se puede comprobar contra una contabilidad en memoria mientras el código de producción usa la real sin cambios. Después se ejecutó tres veces contra las fuentes públicas reales.

RondaPeticionesQué ocurrióTiempo
En frío47.066 avisos guardados, 31,4 MB tras descomprimir18,7 s
Sin cambios1304 Not Modified, validadores guardados conservados, cero escrituras0,3 s
Cursor dos días atrás3355 avisos actualizados, ninguno nuevosegundos

El 304 es el sentido del ejercicio. Un catálogo de avisos servido de forma estática responde a una petición condicional con «nada ha cambiado», y el comportamiento correcto es conservar los validadores guardados, no escribir nada y contar la ronda como exitosa.

Algunos tamaños merecen saberse, porque justo ahí se adivina: el catálogo de vulnerabilidades explotadas activamente ocupa unos 188 kB en el cable porque va comprimido, y una página de dos mil registros de la fuente mayor son unos 18,7 MB. Ambos se leen a través de un techo duro de bytes y no de la ayuda JSON corriente, que no tiene límite alguno; son las dos respuestas más grandes que ingiere la plataforma y vienen de equipos que no gobernamos.

Una ventana que se parte en dos en lugar de saltar

La fuente mayor se pagina dentro de una ventana temporal móvil. Dos cosas fallaban en ese ciclo, y ambas perdían datos sin avisar nunca.

Si una ventana contenía más registros de los que permitía el techo de páginas, el ciclo se detenía y aun así movía el cursor al final de la ventana. Todo lo que quedaba más allá del techo se saltaba en silencio, y solo se habría recogido si otro cambio hubiera vuelto a tocar esos mismos registros.

Y una página vacía en mitad de una ventana, cuando aún se declaraban más registros, adelantaba el cursor una página entera. Dos mil registros perdidos.

Ambos se tratan ahora con el mismo principio: si la respuesta no cabe, hacer una pregunta más pequeña, y si la respuesta es imposible, no mover nada.

Una ventana demasiado grande se parte en dos en lugar de truncarse, hasta seis veces, con un suelo de una hora. Seis particiones convierten una semana en poco más de dos horas, suficiente para cualquier retraso realista. Por debajo de una hora se detiene: una ventana de minutos que sigue devolviendo veinte mil registros no es un problema de ventana sino una avería, y no escribir y dejar el cursor donde está resulta ser la única respuesta honrada disponible. Una página vacía a mitad de ventana se trata igual, como un error que deja el cursor en paz, para que la ronda siguiente lo intente otra vez en lugar de pasar por encima de un hueco.

La partición ocurre después de escribir la página, no antes. Lo ya leído pertenece a la ventana original y es válido; descartarlo cambiaría un tipo de pérdida de datos por otro.

Una cosa menor de la misma zona, digna de repetirse porque bajo presión sale mal con facilidad: la clave de acceso va en una cabecera y jamás en la cadena de consulta. Las URL acaban en registros, en cachés intermedias y en informes de error. Las cabeceras no.

Lo que anotaríamos

  1. Tipar el parámetro, y más si es un estado. Una comparación de texto con una palabra de la enumeración de otra tabla compila, corre y toma para siempre la rama equivocada.
  2. Dos mensajes sobre un mismo suceso llegan en desorden. Sujetar al escritor secundario a los estados de los que puede salir.
  3. Una restricción de unicidad que ignora estados se encontrará con código que no lo hace. Recuperar también las filas cerradas, o la primera reapertura será una violación.
  4. Preguntarse qué envenena un solo fallo. Una excepción que escapa de un bucle por elemento convierte una fila mala en una parada de toda la plataforma, y la condición no se despeja sola.
  5. Una petición condicional respondida con 304 es un éxito. Conservar los validadores, no escribir nada y no contarlo como fracaso.
  6. Si una ventana es demasiado grande, partirla; si una página es imposible, detenerse. Adelantar un cursor sobre datos no leídos es el único desenlace sin vuelta atrás.
  7. Un proceso que no corre sin base de datos se probará contra la base de datos de uno. Una costura de almacenamiento sale más barata que la limpieza.

Para que quede claro: el escaneo de exposición está construido y detrás de un interruptor de despliegue, de modo que ningún cliente lo tiene delante. La reevaluación, las fuentes y el reparto entre varias direcciones están ejercitados; queda sin demostrar el estrechamiento de ventana contra una fuente viva en lugar de una inyectada, el comportamiento del índice sobre un catálogo realmente lleno, y un escaneo lanzado desde una máquina distinta de la que lo pidió.

Del trabajo de plataforma en Provibr. Por qué el comparador se niega a estar seguro de una versión está en una coincidencia que se niega a estar segura, y por qué la sonda de detrás casi no envía nada está en por qué un escáner de vulnerabilidades debería enviar cero bytes.

Empiece con un host y un agente

Cree una cuenta, instale el agente con un solo comando y conecte su primer host de Proxmox. El periodo de prueba dura 14 días y no pide ningún dato de pago.