Transcript
00:00:00Aconteceu de novo. Este é o meu terceiro vídeo sobre CVEs de Server Components este ano e eu nem
00:00:05acho que cobri todos eles. Desta vez são 13 CVEs, sim, 13 deles, entre React
00:00:11e Next.js, 6 dos quais são de alta gravidade e incluem negação de serviço, bypass de middleware,
00:00:15cross-site scripting e muito mais. Talvez os Server Components tenham sido um erro.
00:00:20Então, aqui está a nota de segurança do Next.js, sabe, apenas corrigindo alguns problemas casuais aqui
00:00:28que eles tiveram este mês e, lá no final, obviamente, a resolução é atualizar
00:00:32todas as suas versões do Next.js e estas são as versões impactadas. Vale notar que
00:00:36o TanStack não é impactado por isso, o que — posso ser tendencioso — mas é outro motivo para eu usar
00:00:41o TanStack. Agora, não passarei por todos estes, pois provavelmente ficaríamos aqui por um bom tempo
00:00:44e também não encontrei exploits funcionais para todos, mas quero mostrar um
00:00:48de cada categoria e começaremos com um bypass de middleware e proxy, e o que eu
00:00:52consegui recriar é este do Pages Router. Então temos um bypass de proxy de middleware no
00:00:56Pages Router se você estiver usando i18n, e você pode ver que este é um CVE com uma gravidade
00:01:02de 7.5 de 10. Este é um exemplo de uma aplicação vulnerável, então no Next.js config eu
00:01:06ativei o i18n e também configurei dois locais, inglês e francês, e também tenho um arquivo de middleware
00:01:12que foi renomeado para “proxy” em versões posteriores do Next.js para tentar evitar
00:01:16a confusão que estou prestes a mostrar, mas essencialmente o que o middleware deve nos permitir
00:01:19fazer é modificar uma requisição de entrada, seja redirecionando, reescrevendo ou adicionando
00:01:24alguns headers ou algo do tipo. No meu caso, estou usando para que, se tentarem visitar a
00:01:28página /secret, ele verifique se têm um cookie de sessão, ou seja, se estão logados, e se
00:01:32não estiverem, deve redirecioná-los para a página de login, para que apenas usuários autorizados vejam
00:01:37minha página secreta. Na parte inferior também temos um “matcher” para que qualquer middleware que
00:01:41estamos aplicando a essa página secreta também corresponda às variantes de local, porque tecnicamente
00:01:45agora, como temos dois locais, temos três versões desta URL. Na própria página secreta,
00:01:50eu também tenho algumas server-side props; estas devem ser buscadas no servidor em tempo de renderização
00:01:54e, novamente, como configuramos o middleware, teoricamente apenas um usuário logado deveria ser capaz de ver
00:01:58quais são esses valores, que eu uso mais tarde na própria página, como um e-mail, uma flag e
00:02:03também uma manchete. Repetindo, apenas um usuário autorizado deve poder ver isso. Vamos colocar isso
00:02:07à prova então, e a primeira coisa que tentarei fazer é acessar essa página secreta e você
00:02:11vê que sou redirecionado para o login, já que não estou logado, o que significa que nosso middleware está funcionando,
00:02:16mas e se nos tornássemos hackers mestres? Bem, poderíamos fazer isso primeiro inspecionando o elemento,
00:02:20coisa de hacker absolutamente louca, e então aqui embaixo, no script next data, precisamos procurar
00:02:24pelo nosso build ID; no meu caso é este aqui, e podemos ir em frente e copiá-lo,
00:02:28então precisamos digitar uma URL que é underscore next barra data barra o build ID que
00:02:32acabamos de copiar e então a página que estamos tentando acessar ponto JSON. Feito isso,
00:02:37você pode ver que recebemos de volta aquelas props que deveriam estar protegidas pelo middleware,
00:02:40que no meu caso eram uma flag, e-mail, manchete e também algo dizendo para você se inscrever para
00:02:44mais notícias de desenvolvedores, tutoriais, dicas e truques. Então faça isso. Espero ter
00:02:48impressionado você com minhas habilidades loucas de hacker, mas por que isso realmente acontece? Bem, este é
00:02:52tão fácil de explicar quanto foi de fazer: tínhamos nossa página secreta e algumas server side props,
00:02:56no Next.js as server side props são servidas de uma URL parecida com esta, mas nosso middleware
00:03:00deveria estar protegendo essa rota. O problema é que, como estávamos usando i18n, também
00:03:05tínhamos outras duas URLs, que eram as variantes em inglês e francês; você pode ver que as
00:03:09server side props também ganham variantes em inglês e francês, e o Next.js tinha um código
00:03:13defeituoso que significava que, se tivéssemos o i18n ativado, ele não protegia o caso base, então este
00:03:18não estava incluído no matcher, mas os outros dois estavam, as versões em inglês e francês estavam
00:03:22protegidas, mas não o caso base de barra secret. Podemos ver isso rapidinho se eu apenas mudar
00:03:26esta URL para a versão em inglês: eu sou redirecionado para a página de login. Uma vulnerabilidade incrivelmente simples,
00:03:31mas serei honesto com você: esses bypasses de middleware costumam parecer muito piores do que
00:03:35realmente são; não são ótimos, mas você não deveria proteger muita coisa apenas com middleware de qualquer forma,
00:03:40e o Next.js nem recomenda que você faça isso. Se você tivesse dados sensíveis
00:03:44nessas server side props e não tivesse nenhum tipo de lógica de autenticação no servidor, bem, sinto que parte
00:03:48do problema é sua também; então vamos passar para um mais prejudicial, que é negação de
00:03:53serviço. Houve três destes, mas só houve um que eu consegui recriar de forma confiável,
00:03:56que foi este aqui: negação de serviço com Server Components, e isso impacta o Next.js
00:04:01assim como qualquer coisa usando o pacote react-server-dom, que é basicamente apenas o Next.js
00:04:05e os outros frameworks que o copiaram como o Vinxt e alguns outros forks. O TanStack
00:04:10Start não usa isso, então não é vulnerável. Você pode ver que este também tem uma gravidade
00:04:14de 7.5 de 10. Com este, tudo o que você precisa é de uma aplicação Next.js muito simples e,
00:04:18nela, você precisa estar usando uma Server Action, mas, novamente, pode ser uma bem simples. Este é
00:04:22o site funcionando e você pode ver que, quando eu atualizo a página, o carregamento é
00:04:25quase instantâneo e, para colocar alguns números reais nisso, se eu enviar esta requisição,
00:04:29você vê que ela resolve em 0.02 segundos, mas se eu agora executar meu exploit e então
00:04:34enviar aquela requisição novamente, desta vez levou seis segundos, e isso foi eu executando o exploit
00:04:39uma vez, então imagine o que aconteceria se eu o encadeasse. Agora, para entender este exploit, precisamos
00:04:42saber um pouco sobre o protocolo React Flight, que é o formato que o React usa
00:04:46para serializar árvores de componentes e dados entre o servidor e o cliente. Você provavelmente
00:04:50já viu isso antes: nesta página tínhamos um formulário que tinha uma Server Action. Se eu for
00:04:54até a aba Network aqui e clicar em enviar, você verá que o payload é enviado como
00:04:58dados que parecem bobagem e o mesmo acontece com a resposta aqui. Se formos lá e copiarmos
00:05:02este payload, posso explicar o que acontece quando ele é enviado ao servidor. O primeiro
00:05:05passo é a desserialização e ela começará no chunk 0 onde temos este $k1. Este $k1
00:05:10é na verdade apenas um ponteiro dizendo que haverá alguns dados de formulário aqui
00:05:16que começam com um underscore, então ele pegará todas as outras chaves que enviamos
00:05:20como parte desse payload, passará por todas elas e procurará por uma string que comece com
00:05:24um underscore e saberá que esta será a chave e que este é o valor. Então, assim que
00:05:28terminar, ele pode dizer que temos name, email, message e simplesmente transformará esses dados
00:05:32neste objeto que temos aqui embaixo. Simples e prático. O problema com essa abordagem, porém,
00:05:36é o que acontece quando a escalamos. Digamos que eu adicione outro ponteiro, desta vez procurando por $k2,
00:05:41isso vai procurar por todas as chaves que começam com dois underscores. O problema
00:05:44é que agora, quando estivermos em $k1, ele passará por todas essas seis procurando pelas
00:05:48que começam com um underscore e, quando for para $k2, fará exatamente a mesma
00:05:52coisa, mas procurando as que começam com dois underscores. Então agora estamos passando por 12
00:05:56chaves no total. Bem, isso não é tão ruim, mas vamos levar ao extremo. Se adicionarmos 199.999
00:06:03chaves aleatórias ao payload que estamos enviando e então mudarmos nosso array no zero aqui
00:06:07para ir de $k1, $k2 até $k1000, isso significa que ele terá que procurar por um underscore,
00:06:12dois underscores, três underscores, até mil underscores em todas as nossas 200.000
00:06:17chaves que temos no payload, e isso significa que, no total, ele fará 200 milhões de comparações
00:06:21de strings. Como você pode imaginar, isso vai bloquear a thread por alguns bons segundos.
00:06:25Este é o commit que acredito ter corrigido o problema que estávamos tendo. Você pode ver que
00:06:28há muita coisa acontecendo neste commit e, para ser sincero, é um pouco complexo, mas tentarei
00:06:33explicar da melhor forma. Essencialmente, agora eles usam um sistema baseado em cursor para as chaves,
00:06:36então carregam todas essas 200.000 chaves que enviamos no payload em uma lista e então
00:06:41começamos no zero aqui, onde ele procura pela referência $k1 e começa a descer
00:06:45por essa lista com um cursor que não pode voltar atrás. Ele desce aqui até $j1 e vê que não
00:06:50corresponde ao underscore que precisamos, então vai para $j2, que também não corresponde a um underscore,
00:06:54e continua por toda a lista de chaves até $j199.999. Assim que chega
00:07:01aqui, percebe que não há correspondência para $k1, então passa para $k2. Agora o $k2 começa a procurar
00:07:06por dois underscores, mas o problema é que, como este é um sistema baseado em cursor e este cursor
00:07:09não pode voltar, ele termina imediatamente no fim da lista; então isso também será
00:07:14undefined, e isso continua até $k1000. Desta vez, só passamos por
00:07:18200.000 chaves. Essencialmente, esta correção reduziu o número de operações de
00:07:23$k*n, onde $k é o número de referências $k e $n é o número de chaves, para apenas
00:07:27$n+k. No nosso caso, passamos de 200.000.000 de operações para 201.000, já que
00:07:33ele ainda precisa percorrer todas as chaves e também as referências $k. Eu acho que
00:07:37este tweet do Prime realmente resume a situação em que estamos. Criar seu próprio protocolo com serialização
00:07:41é incrivelmente difícil, então não surpreende que estejamos vendo tantos problemas. Na minha opinião,
00:07:46eles precisam pedir para o Claude Mythos dar uma revisada na base de código do React e do Next.js. A seguir,
00:07:50temos o CV de maior gravidade de todos, que é Server-Side Request Forgery em
00:07:54aplicações Next.js. Você pode ver que este é classificado como 8.6 de 10, mas também vale
00:07:59notar que este não impactou implantações hospedadas na Vercel, apenas auto-hospedadas ou outros provedores.
00:08:04Este exploit também é super simples de aproveitar. Primeiro, precisamos lançar nosso servidor Next.js
00:08:09e, novamente, pode ser uma aplicação Next.js padrão. Você não precisa fazer modificações.
00:08:14Em seguida, também vamos querer um servidor interno. Digamos que este servidor só possa ser acessado pelo
00:08:18servidor Next.js e não pelo mundo exterior. Digamos que ele esteja em nossa nuvem.
00:08:23O que faremos é simplesmente enviar uma requisição curl muito simples para nossa
00:08:26aplicação Next.js. Ela estava na porta 3002 e dizemos que queremos que nosso
00:08:31alvo da requisição seja o servidor ao qual queremos acesso naquela URL de localhost. Se eu agora der
00:08:36enter, veja o que retorna. É na verdade a página HTML de um servidor Python,
00:08:40que tem apenas aquela listagem básica de diretórios, e se eu voltar ao próprio servidor Python,
00:08:45você pode ver que ele teve uma requisição de entrada que veio da aplicação Next.js.
00:08:49Para visualizar melhor o que fizemos, digamos que nesta linha pontilhada está nossa implantação do Next.js,
00:08:53então temos nosso servidor Next.js e também alguns serviços internos, seja Redis,
00:08:57um banco de dados ou qualquer outro serviço, e este aqui você não quer que tenha acesso ao público,
00:09:02para que ninguém possa enviar um curl para ele; ele simplesmente falhará, está protegido por firewall e isolado
00:09:06e só pode ser acessado pelo servidor Next.js. O que fizemos foi simplesmente enviar
00:09:10uma requisição curl para o servidor Next.js e dissemos: “Ei, você pode enviar uma requisição em nosso nome
00:09:15para o serviço interno?” e recebemos a informação de volta; então contornamos o firewall
00:09:19passando pelo Next.js, que tem acesso ao serviço interno. A causa raiz
00:09:23disso também é bem simples: basicamente, no nosso curl enviamos um header de upgrade websocket,
00:09:28e quando enviamos esses headers, o que acontece no Next.js é que chegamos a este trecho
00:09:32de código, e isso resolve as rotas em nossa URL, mas a URL processada que sai aqui é
00:09:37na verdade o alvo da requisição que enviamos em nosso curl, então este será o
00:09:40alvo que estamos tentando alcançar no servidor interno e não a aplicação Next.js.
00:09:45O que acontece com essa URL processada é que ela passa por uma verificação aqui embaixo para dizer: “A
00:09:49URL processada tem um protocolo?” Mas neste caso é sim, já que usamos HTTP e isso é um protocolo,
00:09:55então ele prossegue e faz o proxy dessa requisição para nós. A correção para isso apenas adiciona duas novas
00:09:58guardas em nossa função resolve routes; agora estamos recebendo um booleano de conclusão,
00:10:02bem como um código de status. O que essa função resolve routes realmente faz é
00:10:06pegar nossa URL e processar se é uma requisição de proxy legítima baseada em nossos Next.js
00:10:11rewrites, middleware e coisas do tipo; se não for, significa que o valor de “finished” será
00:10:15falso, então aqui embaixo temos uma verificação: se “finished” for verdadeiro, podemos seguir para o próximo passo; se
00:10:20não for, é definido como falso, então isso não rodará, o que significa que não faremos nossa requisição de proxy
00:10:24e, no caso da requisição curl que tínhamos anteriormente, é exatamente o que vai
00:10:27acontecer: “finished” será falso. Agora, se por algum motivo “finished” fosse verdadeiro, nossa
00:10:32próxima verificação é o código de status; quando resolve routes roda, recebemos um código de status de volta se a
00:10:36solicitação HTTP, então será 200, 404, algo assim; então, se houver um código de
00:10:41isso significa que não é uma requisição de proxy web socket válida, então ele simplesmente ignorará
00:10:45esta linha e não a executará. Tentei realmente mergulhar fundo nessas questões neste vídeo,
00:10:49então me deixe saber se você ainda está assistindo comentando algo aleatório, não sei, como
00:10:53“bar” ou algo assim, e também se inscreva se você gosta do conteúdo. Temos mais duas
00:10:58categorias para passar, mas estas devem ser um pouco mais rápidas, e começarei
00:11:01com envenenamento de cache, onde consegui recriar este problema moderado aqui e veja que este
00:11:04é envenenamento de cache em React Server Component e está classificado como 5.4 de 10. Para recriar
00:11:09este, tenho uma aplicação Next.js, mas também tenho uma CDN falsa para agir como se estivesse
00:11:14em um ambiente real de implantação. Isso significa que, se eu visitar meu site pela primeira vez na
00:11:18URL da CDN e clicar em navegar pelos produtos e voltar e clicar de novo, a primeira vez
00:11:23deve ser um cache miss e na próxima um cache hit. Podemos ver isso acontecendo nos logs
00:11:27aqui: primeiro tivemos um miss em /products com uma query string e depois foi um
00:11:31hit. O que fiz a seguir, porém, foi limpar o cache para simular que talvez o cache tenha expirado
00:11:35ou algo assim, e agora posso enviar esta requisição curl. Com isso de volta na minha aplicação,
00:11:39se eu clicar em navegar pelos produtos agora, verá que recebemos um monte de gíria tecnológica, então
00:11:44fizemos com sucesso um ataque de envenenamento de cache. O que acredito estar acontecendo aqui é que, quando enviamos nosso
00:11:47curl com um header de React Server Components sendo um, esta URL retornará seus dados de
00:11:52Server Component em vez de HTML. Então, quando isso precisa ser cacheado, o Next passará por
00:11:58uma função que verifica se são dados de Server Component ou não. Se forem dados de Server Component,
00:12:02serão armazenados no cache como tal, e se não forem, serão armazenados como HTML. Isso significa que,
00:12:07teoricamente, quando o usuário tenta buscar a versão HTML da página — apenas
00:12:11clicando no botão — ela nunca deveria retornar dados de Server Component. O problema é que, no
00:12:16nosso caso, como tínhamos essa query string no final do nosso curl de Server Component,
00:12:20ela não atendeu aos requisitos para verificar se era um Server Component ou não, já que
00:12:24isso simplesmente verifica se termina em .rsc, mas a nossa versão termina na query string; então
00:12:29agora ele acha que é HTML, armazena no cache como HTML, e da próxima vez que um usuário clicar
00:12:33no botão, ele recebe os dados do Server Component porque o sistema acreditou que
00:12:37aquilo era HTML. A correção para este foi incrivelmente simples: quando fazem a verificação se
00:12:41termina em .rsc, eles simplesmente ignoram as query strings agora. Passando agora para nossa
00:12:46última categoria de CVEs, temos alguns de cross site scripting; este é o que consegui uma recriação,
00:12:50você pode ver que é um 6.1 de 10 e tem cross site scripting em scripts “before interactive”
00:12:55com entrada não confiável. Basicamente, tudo o que isso significa é que no Next.js, se eu tiver uma tag de
00:12:59script que tenha a estratégia de “before interactive”, se eu tiver outro atributo nela que
00:13:03precise de algum tipo de conteúdo não confiável — por exemplo, este vem dos search params —
00:13:08eu posso fazer cross site scripting. Posso fazer isso fazendo o usuário clicar em um link como
00:13:12este, por exemplo, onde incorporei um monte de conteúdo naquele search param; se alguém
00:13:16clicasse naquele link, isso é o que veria: eles poderiam pensar que precisam
00:13:19logar no site novamente e, quando clicarem em entrar, veja aqui que era um formulário de login
00:13:22inteiramente falso injetado via aquele search param; essencialmente permite ao atacante
00:13:26executar JavaScript na máquina da vítima; acho que um exemplo mais realista
00:13:31seria roubar os cookies de sessão do Chrome para logar em tudo o que você tivesse acesso;
00:13:34este é um exemplo bem simples de escape incorreto e podemos ver isso mais fácil com
00:13:39esta versão simplificada, mas tudo o que estou fazendo naquele search param é primeiro fechar uma
00:13:43tag de script, depois abrir uma nova tag de script com o que eu quiser rodar nela; quando damos
00:13:47enter, veja que recebo aquele alerta que diz “pwned”. Como mostrei com meu app,
00:13:51a maneira como este funciona é: precisamos de uma tag script do Next.js com estratégia “before interactive”
00:13:55e também algum atributo nessa tag script que receba seus dados de alguma fonte
00:13:59não confiável; no meu caso, isso vem dos parâmetros de busca. O que isso faz no Next.js é que esta
00:14:04tag script é transformada em algo como isto, onde temos um “dangerouslySetInnerHTML” —
00:14:08já está no nome que vai ser perigoso — e também temos um JSON.stringify.
00:14:12O importante de saber sobre o JSON stringify é que ele não escapa caracteres HTML,
00:14:17como colchetes de fechamento; então o que está acontecendo aqui é que pegamos todas as tags script
00:14:21que configuramos no Next.js, ele procura pela fonte que será definida aqui e
00:14:24então o resto das props conterá o ID de rastreamento de dados, bem como o valor que
00:14:29definimos em nosso parâmetro de busca; isso é colocado no JSON stringify. Bem, o que isso
00:14:33realmente renderiza na página é algo assim: temos nosso ID de rastreamento de dados, que
00:14:37era o resto das props, mas também temos a string que inserimos via parâmetros de
00:14:41busca; então, se expandirmos como isso realmente fica na página, parece algo
00:14:45assim: temos nossa tag script, temos um ID de rastreamento de dados, mas logo depois
00:14:49temos uma tag de script de fechamento; então encerra o script que o Next.js estava tentando fazer e, depois
00:14:53disso, podemos rodar qualquer script que quisermos na página. Também temos este pedacinho
00:14:58no final aqui porque, se não tivéssemos, o analytics seria renderizado na
00:15:01página como texto, já que o HTML veria isso como texto; isso essencialmente apenas
00:15:05engole isso, para que não haja sinais claros de que algo ruim está acontecendo na página. E pronto,
00:15:09são 13 CVEs para o Next.js e React esta semana, vimos como alguns deles funcionam e, honestamente,
00:15:14não sei o que pensar sobre isso. Eu detesto isso, e digo isso como alguém que, há uns
00:15:18dois anos, fazia todo projeto em Next.js e achava que era o melhor e o
00:15:22futuro; mas parece ser um obstáculo atrás do outro. Sinto que eles se apressaram em
00:15:26algumas coisas e depois tiveram que consertar. Pessoalmente, agora estou totalmente focado em TanStack e também
00:15:31Astro quando preciso de um site baseado em conteúdo; eles me parecem muito mais simples e, para ser sincero,
00:15:35também tenho gostado muito do que a Cloudflare tem feito ultimamente, então estou migrando lentamente
00:15:39meus projetos para lá, mas ainda tenho cerca de 20 deles na Vercel e preciso ir
00:15:43atualizar. O que você acha? Os Server Components serão úteis algum dia ou tentamos e falhamos?
00:15:48Deixe-me saber nos comentários abaixo, inscreva-se e, como sempre, vejo você
00:15:51no próximo.