Segurança

Ao receber uma requisição de notificação de um evento, é muito importante validar se a requisição realmente saiu da Kobana e não foi forjada por um terceiro.

Cenário de intrusão

Vamos considerar que a integração que você está desenvolvendo seja da Kobana com um sistema de e-commerce. Ao receber o evento bank_billet.paid, que ocorre quando um boleto é pago, o sistema de e-commerce libera o pedido para que a entrega da mercadoria seja feita.

Imagina que um hacker descobre qual a URL do sistema de e-commerce que recebe as notificações e envia uma notificação forjada, como se fosse a Kobana enviando. Nesse caso, o evento do boleto sendo pago não aconteceu, mas o seu sistema irá liberar o pedido da mesma forma.

Como se proteger

Para se proteger deste tipo de ataque, é necessário implementar uma validação antes do processamento de todas as requisições, que certifica que a requisição foi enviada pela Kobana.

Todas as requisições feitas pela Kobana vão com uma assinatura no cabeçalho X-Kobana-Signature. A assinatura é um string criptografado que é basedo no conteúdo da requisição e na Chave Secreta do webhook.

Para validar se a requisição é real, é necessário gerar a assinatura e comparar com a assinatura que está no cabeçalho da requisição. Caso as assinatura recebida seja igual a assinatura gerada, a requisição é válida e segura.

Um hacker, sem ter acesso a Chave Secreta, não consegue gerar a assinatura e por consequência não consegue forjar a requisição.

É muito relevante manter a Chave Secreta segura, ou seja, não colocando no código fonte do sistema. É recomendável armazenar como variável de ambiente no servidor de produção ou sistema de configuração que seja seguro e criptografado.

Qualquer pessoa com acesso à Chave Secreta é capaz de forjar requisições como sendo vindas da Kobana. Se você acredita que a Chave Secreta vazou de alguma forma, é aconselhável renovar a chave na página de exibição dos dados do webhook.

Chave Secreta do webhook

Para pegar a Chave Secreta do Webhook, vá até a página de Webhooks no menu Integrações -> Webhooks -> Contas e selecione o Webhook em questão.

Clique em copiar no botão azul.

Exemplos de códigos

# O exemplo abaixo está usando o framework minimalista em Ruby,
# chamado [Sinatra](http://www.sinatrarb.com/).

require 'sinatra'
require 'json'

post '/callbacks/kobana' do
  verify_signature

  payload = JSON.parse(request_body)
  "Event Code: #{payload['event_code']}"
end

def request_body
  @request_body ||= request.body.read.to_s
end

def secret_key
  ENV['WEBHOOK_SECRET_KEY']
end

def signature_from_request
  request.env['HTTP_X_KOBANA_SIGNATURE'].split('=').last
end

def generated_signature
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, request_body)
end

def verify_signature
  return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature_from_request, generated_signature)
end
define('WEBHOOK_SECRET_KEY', 'my_shared_secret');

function verify_webhook($data, $hmac_header)
{
  $calculated_hmac = hash_hmac('sha256', $data, WEBHOOK_SECRET_KEY, true);
  return ($hmac_header == $calculated_hmac);
}

$hmac_header = explode("=", $_SERVER['HTTP_X_KOBANA_SIGNATURE'])[1];
$data = file_get_contents('php://input');
$verified = verify_webhook($data, $hmac_header);
error_log('Webhook verified: '.var_export($verified, true)); //check error.log to see the result
// Exemplo C# desenvolvido por Davi Kendy Yorozuya
private string ObterChave(string key, string message){
  Encoding encoding = Encoding.UTF8;

  var keyByte = encoding.GetBytes(key);
  using (var hmacshaSHA256 = new HMACSHA256(keyByte)){
 hmacshaSHA256.ComputeHash(encoding.GetBytes(message));

  return ByteToString(hmacshaSHA256.Hash);
  }
}

public string ByteToString(byte[] buff){
  string sbinary = "";
  for (int i = 0; i < buff.Length; i++)
  sbinary += buff[i].ToString("X2"); /* hex format 
  */
  return sbinary;
}

//Desenvolvido por Maria Paula
//middleware para capturar o rawBody
app.use(function (req, res, next) {
    req.rawBody = "";
    req.on("data", (chunk) => {
      req.rawBody += chunk;
    });
    next();
  });

//validar assinatura
const compareSha = (req, webHookSecretKey) => {
  const requestSignature = req.get("x-kobana-signature").split("=")[1];

  const computedSignature = crypto
    .createHmac("sha256", webHookSecretKey)
    .update(req.rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(computedSignature, "utf8"),
    Buffer.from(requestSignature, "utf8")
  );
};

Algumas observações são importantes:

  • A assinatura é gerada seguindo o padrão HMAC com SHA256;
  • A assinatura enviada no X_KOBANA_SIGNATURE sempre começa com sha256= e deve ser usado o valor após o = na comparação entre a nossa chave e a chave que será gerada por você;
  • A assinatura deve ser gerada usando a Chave Secreta do Webhook, que é individual e única por Webhook e por ambiente(Sandbox ou Production), e o conteúdo(body) da requisição POST enviado em RAW(sem qualquer pré-tratamento por parte do seu servidor ou lib) (request.body);
  • A Chave Secreta não deve ser colocada hard-coded no código fonte e é recomendável que seja armazenada em uma variável de ambiente;
  • Não é recomendável usar o operador == para comparar a assinatura recebida e a assinatura gerada. O método como Rack::Utils.secure_compare executa uma comparação segura contra alguns tipos de timing attacks. Investigue como fazer uma comparação segura na linguagem que estiver utilizando.