Saltar al contenido
Development

Convertir un selector de imagen destacada solo-Pexels en un registro de proveedores (y la revisión que detectó una falsificación de licencia)

Por Victor Da Luz
railsrubysecuritydev-logblog-manager

Tomé esto planeando refactorizar el selector de imagen destacada de blog-manager, de solo-Pexels a algo donde pudieran conectarse Unsplash y Openverse más adelante. Lo que realmente saqué de esto fue una buena lección sobre por qué una revisión automatizada multi-ángulo se paga sola incluso en un refactor “aburrido”.

Lo que intentaba hacer

El selector solo hablaba con Pexels: un cliente, una ruta, una vista. Otros dos proveedores (Unsplash, Openverse) ya estaban registrados como issues separados, ambos bloqueados por este. Lo que se pedía era extraer una capa agnóstica de proveedor de debajo del código específico de Pexels, para que esos dos issues tuvieran algo donde conectarse, sin cambiar nada visible para el usuario, Pexels tenía que seguir funcionando exactamente igual.

Lo que construí

Un pequeño módulo de registro, ImageSearch: un hash PROVIDERS indexado por nombre de proveedor, donde cada entrada tiene una verificación configured? y un lambda build que construye el cliente de ese proveedor a partir de la configuración de la app. ImageSearch.search distribuye la búsqueda entre todos los proveedores configurados, guarda en caché los resultados de cada uno por separado, y los intercala en round-robin para que ningún proveedor solo desplace a los demás una vez que hay más de uno.

Pexels::Client#search ahora devuelve una forma de hash normalizada en vez de los objetos de foto crudos de la API de Pexels, provider, id, urls, credit, license, y un bucket extras para cosas específicas de cada proveedor. Las acciones del controlador se renombraron de search_pexels/select_pexels_image a search_images/select_image, y un nuevo partial compartido _hero_attribution.html.erb reemplazó dos copias del mismo markup “Photo by X on Y”.

Una decisión que salió de leer el código, no el issue

La sección de alcance del issue pedía una migración que agregara las columnas hero_image_source y de licencia. Fui a escribirla y descubrí que ya existía, un issue anterior, unos días antes, había agregado exactamente esas mismas columnas más un backfill para las imágenes destacadas de Pexels ya existentes. Las descripciones de los issues envejecen; prefiero detectar eso leyendo db/schema.rb antes de escribir una migración que escribir una duplicada y que choque en la revisión.

El issue también especificaba un footer codificado: “Photos provided by Pexels, Unsplash and Openverse.” En su lugar lo construí a partir de ImageSearch.configured_providers, ya que Unsplash y Openverse todavía no tienen clientes reales, dar crédito a dos proveedores que en realidad no pueden devolver una foto se sentía peor que un footer que simplemente crece junto con el registro.

Lo que la revisión detectó

Corrí una revisión automatizada de 8 ángulos antes de hacer merge (línea por línea, auditoría de comportamiento eliminado, trazado cruzado de archivos, reutilización, simplificación, eficiencia, altitud, convenciones), y luego verifiqué cada candidato de forma independiente antes de actuar sobre él. Dos de los ángulos encontraron lo mismo desde direcciones distintas, y era peor de lo que yo había notado al escribir el código: select_image escribía params[:provider] directamente en la columna enum sin ninguna verificación. La falla obvia es un 500 sin manejar ante un valor inválido. La que no había captado: como openverse es un valor válido del enum que también activa el auto-hospedaje, una solicitud podía combinar provider=openverse con una URL de imagen de Pexels y un texto de licencia falsificado, y la app descargaría y confirmaría esa imagen en el repositorio público del blog creyendo que era legítimamente auto-hospedable. El mismo error, con un radio de impacto mucho peor una vez que se lo sigue un nivel más.

Un segundo hallazgo: el propio comentario del código nuevo prometía que un proveedor que falla “never fails the whole search,” pero la cláusula rescue solo capturaba mi propia jerarquía ImageSearch::Error, un timeout real de Pexels o una respuesta JSON malformada habrían pasado de largo igual y devuelto un 500 de todas formas. La promesa era aspiracional, no cierta en realidad.

Ambos se arreglaron antes del merge: una verificación de whitelist contra Post.hero_image_sources antes del update, y un rescue ampliado que también captura las fallas a nivel de transporte que Pexels puede lanzar de verdad.

Lo que me sorprendió

Cuánto más contundente resulta “un parámetro de proveedor falsificado podría confirmar una imagen con licencia incorrecta en el blog en vivo” frente a “un parámetro inválido causa un 500.” Misma causa raíz, misma solución de una línea, pero el ángulo de revisión que lo enmarcó alrededor de la consecuencia real (falsificación de licencia, no solo un crash) es la versión que me habría hecho parar y arreglarlo de inmediato incluso en un día apurado. Enmarcar el escenario de falla de forma concreta, no solo nombrar la validación faltante, resultó importar más de lo que esperaba.

También vale la pena recordar para la próxima: uno de los pases verificadores paralelos de la revisión chocó con un rate limit a mitad de la verificación, lo dijo explícitamente en vez de seguir en silencio como si hubiera obtenido una segunda opinión, y continuó apoyándose en sus propias lecturas directas de archivos. Algo pequeño, pero es la diferencia entre una herramienta en la que se puede confiar y una que hay que verificar dos veces.

Qué sigue

Unsplash y Openverse son los dos issues para los que se construyó esto, serán la primera prueba real de si los puntos de extensión del registro (los lambdas configured/build, la forma de resultado normalizada) aguantan más allá de un solo proveedor. La revisión también señaló que ImageSearch::PROVIDERS y Post::HERO_IMAGE_SOURCE_INFO ahora llevan cada uno un nombre de proveedor para mostrar, en dos hashes separados que ya se desincronizaron una vez (Unsplash/Openverse están en uno pero no en el otro), vale la pena colapsarlos en una única fuente de verdad antes de que un tercer proveedor lo empeore.

Lecturas relacionadas

Development

Tres líneas de configuración, una tarde de verificación

Descomentar las flags de SSL de Rails tomó diez minutos. Leer el código fuente del framework, poner a prueba dos hallazgos de revisión que sonaban plausibles pero eran incorrectos, y comprobar la cookie en producción se llevó el resto.

Leer