Ir para o conteúdo

Blogue ·

Um URL de webhook que é o segredo

Um webhook de entrada pode ser a própria credencial se o URL suportar esse peso. O custo: 160 bits, uma assinatura sobre o relógio e um corpo cortado cedo.

Por Provibr

Alguém quer que a sua ferramenta de monitorização inicie um dos nossos fluxos de automação. Essa ferramenta sabe enviar JSON para um URL, e mais nada: sem assinatura, sem cabeçalho próprio, sem certificado de cliente. É um equipamento num bastidor, configurado em 2019, e não vai aprender nada de novo.

É possível recusar esse cliente, ou aceitar que para algumas fontes o próprio URL é a credencial. Aceitámos, e depois passámos a ronda inteira a garantir que esse URL suporta mesmo tal peso em vez de se limitar a ser comprido.

Trinta e dois caracteres, e nunca derivados de nada

O identificador em /api/hooks/flows/<slug> tem trinta e dois caracteres de um alfabeto de trinta e dois símbolos: 160 bits de entropia em bruto. Não é derivado da conta, do fluxo nem do nome do ponto de entrada, porque um identificador calculável a partir do que um cliente já publicou é calculável por quem quer que leia a sua página de estado.

O alfabeto é o base32 de Crockford: os dígitos e as minúsculas menos i, l, o e u. A mesma escolha dos nossos códigos de ativação de agentes, pela mesma razão. Esta cadeia é copiada para o painel de administração de outra pessoa, e nenhum tipo de letra torna l e 1 distinguíveis quando alguém está cansado.

O segredo é cifrado, não resumido

Um ponto de entrada pode ainda levar um segredo partilhado, como assinatura HMAC ou como testemunho bearer. Ambos ficam guardados cifrados, e isso merece explicação, porque o instinto construído por duas décadas de conselhos sobre palavras-passe está aqui exatamente errado.

Uma palavra-passe pode ser resumida porque basta responder se é a mesma cadeia. Um segredo HMAC não: verificar significa calcular a assinatura, e para isso é preciso o texto em claro. Entra portanto sob AES-256-GCM, com uma chave derivada do segredo da plataforma e com dados autenticados que nomeiam o ponto de entrada a que pertence, de modo que um texto cifrado retirado de uma linha não se verifica como segredo de outra. É a mesma maquinaria que já protege as chaves de pagamento e os segredos dos guiões; um segundo mecanismo significaria uma segunda história de rotação e um segundo sítio onde errar.

Se essa fonte de chave mudar algum dia, o texto cifrado não abre e o pedido é recusado em vez de aceite; o ecrã diz então que o segredo tem de ser gerado de novo. Falhar em aberto perante um segredo ilegível é do género de linha que se escreve uma vez à pressa e se encontra três anos depois.

O cliente vê o segredo uma única vez, ao criá-lo. Não por não o podermos mostrar outra vez, mas porque «anotado agora em dois sítios» e «visível para qualquer pessoa com permissões de automação» são riscos diferentes, e só o primeiro é inevitável.

O relógio entra no material assinado

A assinatura é sha256=<hex> num cabeçalho, calculada sobre a marca temporal, um ponto e o corpo em bruto. A marca viaja num cabeçalho próprio e tem de cair dentro de cinco minutos do nosso relógio, dos dois lados.

Pôr a marca dentro do material assinado em vez de ao lado é todo o mecanismo contra a repetição. Se só o corpo for assinado, quem tiver captado um pedido válido pode repeti-lo para sempre com um cabeçalho temporal novo, e a janela fica a enfeitar. Os dois lados contam igualmente: uma verificação que só recusa o demasiado antigo aceita uma assinatura datada de 2099.

A comparação corre em tempo constante, porque uma igualdade vulgar sobre texto hexadecimal termina no primeiro carácter diferente, e esse tempo basta para adivinhar uma assinatura byte a byte. E a marca só é julgada depois de a assinatura bater certo, nunca antes. Ao contrário, «expirada» seria um oráculo gratuito: alguém sem qualquer segredo válido ficaria a saber que o problema era o seu relógio e não a sua assinatura. A distinção entre assinatura errada e assinatura expirada existe, mas existe no recibo que o dono do ponto de entrada lê, não na resposta dada a quem chama.

Um corpo cortado enquanto chega

O tecto do corpo é de 64 KiB por omissão, e o que conta não é o número mas o sítio onde é aplicado.

Ler o pedido inteiro para uma cadeia e só depois verificar o comprimento é uma verificação posterior ao dano: um corpo de 500 MB custa-nos 500 MB antes da recusa, e assim o tecto passa a convite. O corpo é por isso lido aos bocados a partir do fluxo, o total corrente é comparado a cada bocado, e o leitor é cancelado assim que o ultrapassa. Os bytes registados são os que lemos de facto, não a Content-Length que a fonte declarou, porque quem nos ataca é justamente aquele cujo cabeçalho não convém acreditar.

O travão de caudal fica depois do corpo e não antes, ao contrário do conselho habitual, e tem de ser assim: a assinatura calcula-se sobre o corpo, logo um travão que responda antes não pode saber se o pedido era legítimo. Conta por ponto de entrada e não por endereço de origem. Uma fonte real de webhooks tem um endereço; quem ataca tem mil, pelo que travar pelo endereço protege exatamente a parte errada.

Nove recusas, doze desfechos e nenhum corpo

A ordem das recusas é o desenho. Primeiro a forma do identificador, antes de tocar na base de dados, porque where slug = <um megabyte de texto> é trabalho que um desconhecido não devia poder impor-nos. Depois a existência, a revogação, a desativação, a lista de endereços, o tecto do corpo, o travão de caudal, a assinatura e, por fim, se o JSON é sequer um objeto.

Cada recusa a partir do terceiro passo escreve um recibo. Essa tabela é a resposta a «o meu webhook não faz nada», uma frase que esconde dois problemas bem diferentes: não chega nada, ou chega algo e é rejeitado. Sem uma linha os dois são indistinguíveis, e o cliente passa a tarde no problema errado.

O recibo leva o desfecho como palavra própria ao lado do estado HTTP, porque os códigos de estado são má língua para isto: um mesmo 403 cobre «o seu endereço não está na lista» e «este ponto de entrada está desligado», e isso manda uma pessoa para dois ecrãs diferentes. Doze desfechos, apenas acrescentáveis, com o endereço de origem e os bytes lidos.

O recibo nunca leva o corpo: é a carga de outra pessoa, pode conter o que for, e uma comodidade de diagnóstico não compensa uma tabela que acumula em silêncio dados alheios. Os recibos ficam noventa dias, deliberadamente menos do que as execuções a que pertencem, porque um endereço de origem é um dado pessoal de um terceiro que não consentiu nada.

Uma recusa não escreve nada: um identificador desconhecido. Não há conta onde pendurar a linha, e uma tabela que cresce sempre que um desconhecido adivinha é uma forma gratuita de encher a nossa base de dados.

Porque 410 e 404 não são a mesma resposta

Revogar um ponto de entrada não o apaga. A linha fica, o identificador fica ocupado para sempre, e um pedido recebe 410 em vez de 404. O nome volta a ficar disponível, porque distinguir dois pontos de entrada vivos era todo o objetivo dessa unicidade, mas o URL nunca volta.

A alternativa tentadora é responder 404 nos dois casos, para que ninguém saiba que identificadores existiram. Adivinhar um são 160 bits de trabalho, portanto quem tem um recebeu-o, de uma linha de registo, de uma integração antiga ou de uma captura de ecrã, e já sabe que existiu. A distinção não lhe diz nada.

À metade honesta do mundo diz bastante. Uma fonte que continua a enviar para o URL antigo depois de uma rotação vê no 410 que precisa de um novo; perante um 404 conclui que alguém escreveu mal o caminho, e então uma pessoa procura um defeito no caminho em vez de na rotação.

O primeiro despoletador sem servidor

Todos os despoletadores anteriores atuavam sobre uma máquina existente: aprovisionada, reinstalada, eliminada. Uma execução por webhook não tem máquina, nem plano, nem endereço, nem integração, nem agente, nem ninguém com sessão iniciada.

Parece pouco e foi a aresta mais afiada da ronda. O catálogo de variáveis anota em cada grupo os despoletadores para que está disponível, e a constante que significava «sempre» era literalmente a lista de todos os despoletadores. Um quinto valor teria assim oferecido a um fluxo por webhook todas as variáveis de servidor, plano, endereço, rede e agente no seletor; o validador tê-las-ia aprovado, e o primeiro pedido real teria caído ao compor o texto, dentro de uma execução que ninguém observa.

Há agora, portanto, uma lista à parte que responde a outra pergunta: este despoletador atua sobre um servidor? Um sexto despoletador terá de responder primeiro a isso. O que falta numa execução por webhook é podado do contexto em vez de ficar presente e vazio, e um nó de ação num grafo desses é recusado ao gravar.

O corpo aterra a um só nível, como texto, número ou valor lógico. Os campos desconhecidos são saltados em vez de recusados, e esse é o único ponto em que uma carga de webhook difere de uma escrita pelo próprio cliente: uma carga escrita à mão com uma gralha deve falhar, mas a mensagem de um terceiro traz valores nulos, objetos aninhados e listas porque nunca foi escrita para nós, e recusar o pedido inteiro por um campo a mais torna inútil qualquer fonte real.

O que anotaríamos

  1. Se o URL é a credencial, convém fazer dele uma credencial. Aleatório, não derivado, impossível de adivinhar, revogável, nunca reemitido.
  2. Um segredo com que se calcula não pode ser resumido. Cifrá-lo, prendê-lo ao seu dono e falhar em fechado quando não abrir.
  3. Assinar o relógio, não apenas o corpo. Uma marca temporal fora da assinatura é uma defesa que não defende nada.
  4. Aplicar o tecto de tamanho enquanto os bytes chegam. Uma verificação após a leitura já pagou o preço.
  5. Travar o que o cliente configurou, não o que o atacante controla.
  6. Registar as recusas, não a carga. As recusas tornam «não acontece nada» uma pergunta com resposta; a carga é dado alheio.
  7. Se um caso novo não tem sujeito, procurar toda a lista que significava «todos os casos». Uma delas é sinónimo da enumeração, e não o declara.

Para que fique claro: os pontos de entrada, os esquemas, os recibos e o despoletador de webhook estão construídos e a funcionar. Fica por demonstrar o recurso em memória do travão perante uma falha real da cache, o comportamento após trocar o segredo da plataforma, e tudo isso no domínio próprio de um cliente.

Do trabalho de plataforma na Provibr. Como o grafo por detrás do despoletador se mantém finito está em um fluxo é um grafo, e um grafo tem de ser finito, e como a tela ganhou o seu fio está em um fio que se arrasta, e exatamente um gatilho.

Comece com um host e um agente

Crie uma conta, instale o agente com um único comando e ligue o seu primeiro host Proxmox. O período experimental dura 14 dias e não pede quaisquer dados de pagamento.