Script de terceiro é todo arquivo JavaScript que a sua página manda o navegador buscar num domínio que não é seu. Ele não passa pelo
package.json, não aparece no relatório de SCA e roda na página com o mesmo poder que o seu próprio código. O inventário desses scripts só se monta de fora: abrindo a página como um visitante e anotando tudo que ela pede.
Em agosto fui conferir por que o número de visitas do nosso painel não batia com o log do servidor. Levou uma tarde e a resposta foi constrangedora. O csurface.io carregava cinco scripts de terceiro que não estavam em template nenhum do CMS.
Eles entravam por uma camada acima do nosso código, ligada num painel de CDN, e o carregador era a primeira coisa do <head>, antes de qualquer linha nossa. Um deles pertencia a uma ferramenta de marketing que a operação já tinha abandonado. A integração saíra do site no dia anterior. O rastreador continuava carregando para todo mundo que abrisse a home.
Ninguém invadiu nada. Cada um dos cinco foi ligado por alguém, em algum momento, com uma boa razão na cabeça. O que faltava era a lista. E quem fosse procurar essa lista no repositório não ia achar, porque ela não mora lá.
É disso que este texto trata.
O inventário que o package.json não tem
A análise de composição de software lê o manifesto e o lockfile. Ela sabe que a sua build puxou 380 pacotes de npm, sabe a versão de cada um e sabe quais têm CVE aberto. É um inventário bom e resolve um problema real.
Só que o navegador não lê o lockfile. Ele lê o HTML que chegou e busca o que a tag <script src> mandar buscar. Se a tag aponta para um domínio externo, o navegador vai lá, baixa o que estiver servido naquele instante e executa.
São dois inventários diferentes, e a maior parte das empresas mantém só o primeiro.
O segundo tem uma propriedade que o primeiro não tem: ele muda sem que ninguém faça deploy. O pacote de npm que você fixou na versão 4.17.21 continua sendo a 4.17.21 até você mexer. O arquivo servido por cdn.exemplo.com/lib.js é o que o dono daquele domínio decidir servir hoje de manhã.
Por onde o script entra sem passar pelo repositório
Três caminhos cobrem quase tudo que eu já vi.
O gerenciador de tags é o mais comum. Alguém do marketing precisa medir uma campanha, entra no painel, cola o trecho e publica. Não abre chamado, não passa por revisão de código, não deixa rastro no Git. O painel foi desenhado exatamente para isso, e funciona.
A camada de CDN é o caminho que menos gente conhece. Provedores de borda hoje oferecem injeção de tag como um botão de configuração. Ligado o botão, o script entra na resposta antes de o HTML sair do servidor de borda, o que significa que ele não aparece nem no template nem no log da origem. Foi assim no nosso caso.
O terceiro é o código antigo. Uma tag colada num template em 2019 por uma pessoa que já saiu da empresa, para resolver um problema de compatibilidade que também já acabou. Ninguém remove porque ninguém sabe que ela está lá, e ela não quebra nada.
O que os três têm em comum é que o script fica fora do fluxo que a segurança de aplicação monitora. E fica ligado por anos.
CSP bloqueia, e não conta
Quando levanto esse assunto, a primeira reação costuma ser Content-Security-Policy. É uma boa ferramenta e vale ter.
Ela responde outra pergunta, porém. A CSP diz ao navegador de quais origens ele pode carregar código. Se você mantém a lista curta e correta, ela impede que um script novo apareça. O que ela não faz é te dizer quais scripts você já carrega hoje, quem é dono de cada domínio da lista e qual deles trocou de mãos no mês passado.
Some a isso o que acontece na prática. A CSP quase sempre nasce permissiva, com unsafe-inline e curinga, para não quebrar produção na semana do lançamento. Depois ninguém aperta. Uma política com curinga não bloqueia e também não informa.
O diagnóstico precisa vir antes. Você não escreve uma CSP boa sem ter primeiro a lista do que a página carrega, e essa lista é justamente o que falta.
O sinal que faltou no Polyfill.io
O caso Polyfill.io é o exemplo que uso porque cada peça dele é pública e verificável.
O domínio servia polyfills de JavaScript para mais de cem mil sites. Em fevereiro de 2024 ele foi vendido. Em junho, os scripts servidos por ele passaram a redirecionar parte dos visitantes para páginas fraudulentas. Nenhum dos sites afetados foi invadido, e nenhum deles mudou uma linha de código. A tag continuou apontando para o mesmo endereço de sempre.
O que me interessa aqui não é o ataque. É o intervalo.
Entre a venda do domínio e o primeiro script malicioso passaram quatro meses. O criador original do projeto avisou publicamente que o domínio tinha trocado de dono e que não era mais seguro confiar nele. O aviso ficou no ar durante todo esse tempo.
Quem carregava o script não viu o aviso porque não sabia que carregava o script. Esse é o buraco inteiro, e ele é de inventário, não de detecção. Um alerta de mudança de WHOIS num domínio da sua lista teria bastado. Só que o alerta precisa de uma lista para existir.
Como montar a lista
A lista se monta de fora para dentro, e o método é menos sofisticado do que parece.
Você abre a página num navegador de verdade, sem cabeça, deixa ela renderizar por completo e grava toda requisição de rede que ela dispara. Não a fonte do HTML: a página executada, com o que o gerenciador de tags injetou, com o que o script injetado injetou depois, e com o que a camada de borda acrescentou no caminho.
Isso responde a primeira pergunta, que é quais domínios a sua página consulta. Vem então a segunda, que dá mais trabalho e vale mais: quem é dono de cada um desses domínios, e desde quando.
Feito uma vez, o resultado é uma foto. Ela já surpreende, e no nosso caso surpreendeu. Mas o valor está em repetir. A foto de hoje comparada com a de ontem mostra o script que apareceu de madrugada, o domínio que trocou de titular no registro e o certificado que vence em onze dias, o que costuma ser o aviso mais barato que existe.
Uma coisa que aprendi tarde: faça isso a partir de fora da sua rede e a partir de mais de um lugar. Boa parte da entrega maliciosa no caso do polyfill era seletiva, filtrada por geolocalização e horário. Um scanner rodando do datacenter em horário comercial recebia a versão limpa do arquivo.
O que a CSURFACE faz aqui, e o que não faz
Começo pelo que fica de fora, porque é a parte que costuma ser omitida.
A plataforma não lê o seu package.json, não gera SBOM e não faz análise de composição de software. Ela não avalia pacote em registro público, nem npm nem PyPI, e não olha para o seu pipeline de CI/CD nem para a rotação dos seus segredos. Um pacote comprometido lá atrás só entra no campo de visão dela se acabar servido como recurso de uma página sua. Essa cobertura continua sendo de ferramenta de AppSec e de CI/CD, e a plataforma não substitui nenhuma das duas.
O que a plataforma faz é o inventário de fora. Para cada ativo que ela descobre no seu perímetro, ela renderiza a página real, captura as requisições e registra os domínios de terceiro que aquele ativo consulta, com script, CDN e API. Cada entrada carrega data de primeira e de última observação, então o histórico responde desde quando aquele script está ali e quando ele deixou de aparecer. Sobre os domínios da lista, ela acompanha mudança de registro, de DNS e de certificado.
O limite honesto é este: ela enxerga o que a sua página carrega e o que acontece com os domínios dessa lista. É a camada de recurso servido e a de DNS. A profundidade de subfornecedor além disso é conversa de TPRM, e eu não a venderia junto.
Vale para a maior parte do problema do polyfill, porque o problema do polyfill era de inventário e de mudança de dono. Não vale para o Shai-Hulud, que é comprometimento de credencial de mantenedor dentro do registro npm, e não passa pela sua página.
Por onde começar amanhã
Abra a home num navegador, com o painel de rede aberto, e conte os domínios distintos. Só isso já costuma render uma conversa desconfortável.
Depois pergunte, para cada um: quem ligou, quando, e continua sendo necessário. Foi essa terceira pergunta que pegou o nosso rastreador zumbi.
Perguntas frequentes
Qual a diferença entre script de terceiro e dependência de npm?
A dependência de npm entra na sua build e vira parte do arquivo que você publica. Você controla a versão, e ela só muda quando você refaz o deploy. O script de terceiro é buscado pelo navegador do visitante, em tempo de execução, num servidor que não é seu. O conteúdo dele pode mudar a qualquer hora sem aviso e sem deploy do seu lado.
O SCA não resolve isso?
Não. O SCA lê manifesto e lockfile, e portanto só enxerga o que passa pelo gerenciador de pacotes. Script colado num gerenciador de tags, injetado pela camada de CDN ou escrito direto no template não aparece em relatório nenhum de SCA.
Content-Security-Policy resolve?
Ajuda a impedir que entre coisa nova, e não diz o que já está entrando. Uma política escrita antes do inventário costuma nascer permissiva demais para bloquear qualquer coisa. Faça a lista primeiro.
Com que frequência esse inventário precisa ser refeito?
Continuamente, porque a mudança não depende de você. No caso do Polyfill.io, o domínio trocou de dono em fevereiro e o script malicioso apareceu em junho. Uma revisão trimestral teria uma chance razoável de pegar a janela. Uma revisão anual não teria nenhuma.
O que faço quando encontro um script de dono desconhecido?
Verifique primeiro se alguém ainda precisa dele, porque em boa parte dos casos a resposta é não e o problema acaba na remoção. Se for necessário, prenda o que der: integrity com hash quando o arquivo for estável, versão fixa em vez de ponteiro para a última, e o domínio na lista da CSP. E coloque uma checagem de mudança de titular no registro do domínio.
Isso vale para site institucional ou só para aplicação?
Vale mais para o institucional, na minha experiência. É onde o marketing tem acesso ao gerenciador de tags, onde o template é mais antigo e onde a segurança de aplicação olha menos. O portal de login costuma estar mais limpo que a home.