Pular para o conteúdo
Development

Transformando um seletor de imagem de destaque só do Pexels num registro de provedores (e a revisão que pegou uma falsificação de licença)

Por Victor Da Luz•
railsrubysecuritydev-logblog-manager

Peguei esse issue planejando refatorar o seletor de imagem de destaque do blog-manager, que só falava com o Pexels, pra algo que pudesse plugar Unsplash e Openverse depois. O que de fato tirei disso foi uma boa lição sobre por que uma revisão automatizada com vários ângulos se paga sozinha até numa refatoração “sem graça”.

O que eu estava tentando fazer

O seletor só falava com o Pexels: um client, uma rota, uma view. Dois provedores a mais (Unsplash, Openverse) já estavam registrados como issues separadas, ambas bloqueadas por essa aqui. O pedido era extrair uma camada agnóstica de provedor de baixo do código específico do Pexels, pra que essas duas issues tivessem algo pra plugar, sem mudar nada que um usuário pudesse ver, o Pexels ainda tinha que funcionar exatamente igual.

O que eu construí

Um módulo de registro pequeno, ImageSearch: um hash PROVIDERS indexado por nome de provedor, cada entrada carregando uma checagem configured? e uma lambda build que constrói o client daquele provedor a partir das configurações do app. ImageSearch.search distribui a busca entre todo provedor configurado, guarda em cache o resultado de cada um separadamente, e intercala eles em round-robin pra que nenhum provedor sozinho domine os outros quando há mais de um.

Pexels::Client#search agora devolve um formato de hash normalizado em vez dos objetos crus de foto da API do Pexels, provedor, id, urls, crédito, licença, um bucket extras pra coisa específica de cada provedor. As ações do controller foram renomeadas de search_pexels/select_pexels_image pra search_images/select_image, e um novo partial compartilhado _hero_attribution.html.erb substituiu duas cópias do mesmo markup de “Foto de X em Y”.

Uma decisão que veio de ler o código, não a issue

A seção de escopo da issue pedia uma migration adicionando hero_image_source e colunas de licença. Fui escrever e descobri que já existia, uma issue anterior, poucos dias antes, tinha adicionado exatamente essas colunas mais um backfill pras imagens de destaque do Pexels já existentes. Descrições de issue envelhecem; prefiro pegar isso lendo db/schema.rb antes de escrever uma migration do que escrevendo uma duplicada e vendo ela colidir na revisão.

A issue também especificava um rodapé fixo: “Fotos fornecidas por Pexels, Unsplash e Openverse.” Construí a partir de ImageSearch.configured_providers em vez disso, já que Unsplash e Openverse ainda não têm clients de verdade, dar crédito a dois provedores que não conseguem de fato devolver uma foto pareceu pior do que um rodapé que só cresce conforme o registro cresce.

O que a revisão pegou

Rodei uma revisão automatizada de 8 ângulos antes de fazer merge (linha por linha, auditoria de comportamento removido, rastreamento entre arquivos, reuso, simplificação, eficiência, altitude, convenções), e depois verifiquei cada candidato de forma independente antes de agir sobre ele. Dois dos ângulos encontraram a mesma coisa por caminhos diferentes, e era pior do que eu tinha notado escrevendo o código: select_image estava escrevendo params[:provider] direto na coluna enum sem nenhuma checagem. A falha óbvia é um 500 sem tratamento num valor inválido. A que eu não tinha percebido: já que openverse é um valor de enum válido que também ativa o self-hosting, uma requisição poderia combinar provider=openverse com uma URL de imagem do Pexels e um texto de licença forjado, e o app baixaria e commitaria essa imagem no repositório público do blog acreditando que era legitimamente self-hostable. O mesmo bug, com um raio de impacto bem maior quando você segue mais um passo adiante.

Um segundo achado: o próprio comentário do código novo prometia que um provedor falhando “nunca derruba a busca inteira”, mas a cláusula de rescue só pegava a minha própria hierarquia ImageSearch::Error, um timeout real do Pexels ou uma resposta JSON malformada passariam direto por ela e derrubariam a requisição com 500 do mesmo jeito. A promessa era aspiracional, não verdadeira de fato.

Os dois foram corrigidos antes do merge: uma checagem de whitelist contra Post.hero_image_sources antes do update, e um rescue mais amplo que também pega as falhas em nível de transporte que o Pexels de fato pode lançar.

O que me surpreendeu

O quanto “um parâmetro de provedor forjado poderia commitar uma imagem com licença errada no blog ao vivo” pesa mais do que “um parâmetro inválido causa um 500”. Mesma causa raiz, mesma correção de uma linha, mas o ângulo de revisão que enquadrou isso em torno da consequência real (falsificação de licença, não só um crash) é a versão que teria me feito parar e corrigir imediatamente até num dia corrido. Enquadrar o cenário de falha de forma concreta, não só nomear a validação que faltava, acabou importando mais do que eu esperava.

Também vale lembrar pra próxima vez: uma das passadas de verificação paralelas da revisão bateu num rate limit no meio da checagem, disse isso explicitamente em vez de seguir em silêncio como se tivesse conseguido uma segunda opinião, e continuou apoiada na força das próprias leituras diretas de arquivo. Coisa pequena, mas é a diferença entre uma ferramenta em que você pode confiar e uma que você precisa checar de novo.

O que vem a seguir

Unsplash e Openverse são as duas issues pra que isso foi construído, elas vão ser o primeiro teste real de se os pontos de extensão do registro (as lambdas configured/build, o formato de resultado normalizado) aguentam além de um único provedor. A revisão também sinalizou que ImageSearch::PROVIDERS e Post::HERO_IMAGE_SOURCE_INFO agora carregam um nome de exibição por provedor em dois hashes separados que já divergiram uma vez (Unsplash/Openverse estão num mas não no outro), vale colapsar numa fonte única de verdade antes que um terceiro provedor piore isso.

Leitura relacionada

Você também pode achar útil

RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
NordPass

NordPass

Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.

Como afiliado da NordPass, ganho com compras qualificadas.

Saiba mais
AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais