Você deve se proteger contra valores inesperados de APIs externas?

51

Digamos que você esteja codificando uma função que recebe entrada de uma API externa MyAPI.

Essa API externa MyAPIpossui um contrato que declara que retornará a stringou a number.

É recomendado para proteger contra coisas como null, undefined, boolean, etc., mesmo que isso não faz parte da API de MyAPI? Em particular, como você não tem controle sobre essa API, não pode fazer a garantia por meio de uma análise de tipo estática, por isso é melhor prevenir do que remediar?

Estou pensando em relação ao princípio da robustez .

Adam Thompson
fonte
16
Quais são os impactos de não lidar com esses valores inesperados se eles forem retornados? Você pode viver com esses impactos? Vale a pena a complexidade de lidar com esses valores inesperados para evitar ter que lidar com os impactos?
Vincent Savard 14/01
55
Se você os espera, por definição eles não são inesperados.
Mason Wheeler
28
Lembre-se de que a API não é obrigada a devolver apenas o JSON válido (presumo que seja JSON). Você também pode obter uma resposta como<!doctype html><html><head><title>504 Gateway Timeout</title></head><body>The server was unable to process your request. Make sure you have typed the address correctly. If the problem persists, please try again later.</body></html>
user253751 14/01
5
O que significa "API externa"? Ainda está sob seu controle?
Deduplicator
11
"Um bom programador é alguém que olha para os dois lados antes de atravessar uma rua de mão única."
jeroen_de_schutter 16/01

Respostas:

103

Você nunca deve confiar nas entradas do seu software, independentemente da fonte. Não apenas a validação dos tipos é importante, mas também as faixas de entrada e a lógica de negócios. Por um comentário, isso é bem descrito pela OWASP

Não fazer isso, na melhor das hipóteses, deixará você com dados de lixo que você precisará limpar posteriormente, mas, na pior das hipóteses, você terá uma oportunidade para explorações mal-intencionadas se esse serviço upstream for comprometido de alguma forma (qv o Target hack). A variedade de problemas entre eles inclui colocar seu aplicativo em um estado irrecuperável.


Pelos comentários, vejo que talvez minha resposta possa usar um pouco de expansão.

Por "nunca confie nas entradas", quero dizer simplesmente que você não pode assumir que sempre receberá informações válidas e confiáveis ​​de sistemas upstream ou downstream e, portanto, deve sempre limpar essas entradas da melhor maneira possível ou rejeitar isto.

Um argumento apareceu nos comentários que abordarei a título de exemplo. Embora sim, você precisa confiar em seu sistema operacional até certo ponto, não é razoável, por exemplo, rejeitar os resultados de um gerador de números aleatórios se você solicitar um número entre 1 e 10 e ele responder com "bob".

Da mesma forma, no caso do OP, você deve garantir definitivamente que seu aplicativo esteja aceitando apenas entradas válidas do serviço upstream. O que você faz quando não está bem depende de você e depende muito da função de negócios real que você está tentando realizar, mas você o registraria no mínimo para depuração posterior e garantirá que seu aplicativo não funcione em um estado irrecuperável ou inseguro.

Embora você nunca possa conhecer todas as entradas possíveis que alguém / alguma coisa possa fornecer, você certamente pode limitar o que é permitido com base nos requisitos de negócios e fazer alguma forma de inclusão na lista de permissões com base nisso.

Paulo
fonte
20
Qv de quê?
JonH 14/01
15
@ JonH basicamente "veja também" ... o Target hack é um exemplo ao qual ele está referenciando en.oxforddictionaries.com/definition/qv .
andrewtweber 14/01
8
Esta resposta é como está, simplesmente não faz sentido. É inviável prever todas as maneiras pelas quais uma biblioteca de terceiros pode se comportar mal. Se a documentação de uma função de biblioteca garantir explicitamente que o resultado sempre terá algumas propriedades, você poderá confiar nela para que os designers garantam que essa propriedade seja realmente válida. É responsabilidade deles ter um conjunto de testes que verifique esse tipo de coisa e enviar uma correção de bug, caso uma situação seja encontrada onde não. Você verificando essas propriedades em seu próprio código está violando o princípio DRY.
leftaroundabout
23
@leftaroundabout não, mas você deve prever todas as coisas válidas que seu aplicativo pode aceitar e rejeitar o restante.
Paul
10
@leftaroundabout Não se trata de desconfiar de tudo, mas de fontes externas não confiáveis. Isso é tudo sobre modelagem de ameaças. Se você não fez isso, seu software não é seguro (como pode ser, se você nunca pensou em que tipo de atores e ameaças deseja proteger seu aplicativo?). Para uma série de softwares comerciais, é um padrão razoável supor que os chamadores possam ser maliciosos, embora raramente seja sensato supor que seu sistema operacional é uma ameaça.
Voo
33

Sim claro. Mas o que faz você pensar que a resposta pode ser diferente?

Você certamente não deseja que seu programa se comporte de maneira imprevisível, caso a API não retorne o que o contrato diz, não é? Assim, pelo menos você tem que lidar com o comportamento como um de alguma forma . Uma forma mínima de tratamento de erros sempre vale o esforço (muito mínimo!), E não há desculpa para não implementar algo assim.

No entanto, quanto esforço você deve investir para lidar com esse caso depende muito do caso e só pode ser respondido no contexto do seu sistema. Muitas vezes, basta uma pequena entrada de log e deixar o aplicativo terminar normalmente. Às vezes, será melhor implementar algum tratamento detalhado de exceções, lidar com diferentes formas de valores de retorno "errados" e talvez tenha que implementar alguma estratégia de fallback.

Mas faz muita diferença se você estiver escrevendo apenas algum aplicativo interno de formatação de planilha, para ser usado por menos de 10 pessoas e onde o impacto financeiro de uma falha no aplicativo é bastante baixo, ou se você estiver criando um novo carro autônomo sistema, onde uma falha no aplicativo pode custar vidas.

Portanto, não há atalho para refletir sobre o que você está fazendo , usar o bom senso é sempre obrigatório.

Doc Brown
fonte
O que fazer é outra decisão. Você pode ter uma solução de failover. Qualquer coisa assíncrona pode ser tentada novamente antes da criação de um log de exceção (ou letra não utilizada). Um alerta ativo para o fornecedor ou fornecedor pode ser uma opção se o problema persistir.
mckenzm 15/01
@mckenzm: o fato de o OP fazer uma pergunta em que a resposta literal pode obviamente ser apenas "sim" é IMHO, um sinal de que eles podem não estar apenas interessados ​​em uma resposta literal. Parece que eles estão perguntando "é necessário se proteger contra formas diferentes de valores inesperados de uma API e lidar com eles de maneira diferente" ?
Doc Brown
11
hmm, a abordagem porcaria / carpa / matriz. É nossa culpa por passar solicitações ruins (mas legais)? a resposta é possível, mas não utilizável para nós em particular? ou a resposta está corrompida? Cenários diferentes, agora parece dever de casa.
mckenzm 15/01
21

O princípio da robustez - especificamente, a metade "seja liberal no que você aceita" - é uma péssima idéia em software. Ele foi originalmente desenvolvido no contexto do hardware, onde as restrições físicas tornam as tolerâncias de engenharia muito importantes, mas no software, quando alguém envia uma entrada incorreta ou incorreta, você tem duas opções. Você pode rejeitá-lo (de preferência com uma explicação sobre o que deu errado) ou pode tentar descobrir o que deveria significar.

Edição: Acontece que eu estava enganado na declaração acima. O Princípio da Robustez não vem do mundo do hardware, mas da arquitetura da Internet, especificamente da RFC 1958 . Afirma:

3.9 Seja rigoroso ao enviar e tolerante ao receber. As implementações devem seguir as especificações com precisão ao enviar para a rede e tolerar entrada incorreta da rede. Em caso de dúvida, descarte a entrada defeituosa silenciosamente, sem retornar uma mensagem de erro, a menos que isso seja exigido pela especificação.

Isso é, claramente, simplesmente errado do começo ao fim. É difícil conceber uma noção mais equivocada de manipulação de erros do que "descartar entradas defeituosas silenciosamente sem retornar uma mensagem de erro", pelas razões apresentadas nesta postagem.

Veja também o documento da IETF As Consequências Nocivas do Princípio da Robustez para uma elaboração mais aprofundada sobre este ponto.

Nunca, nunca, nunca escolha a segunda opção, a menos que você tenha recursos equivalentes à equipe de Pesquisa do Google para lançar em seu projeto, porque é o necessário para criar um programa de computador que faça algo parecido com um trabalho decente nesse domínio de problema específico. (E mesmo assim, as sugestões do Google parecem estar saindo diretamente do campo esquerdo cerca de metade do tempo.) Se você tentar fazer isso, terá uma enorme dor de cabeça em que seu programa tentará interpretar com frequência entrada ruim como X, quando o que o remetente realmente queria dizer era Y.

Isso é ruim por duas razões. O óbvio é porque você tem dados ruins no seu sistema. O menos óbvio é que, em muitos casos, nem você nem o remetente perceberão que algo deu errado até muito mais tarde, quando algo explode em seu rosto e, de repente, você tem uma grande e cara bagunça para consertar e não faz ideia. o que deu errado porque o efeito perceptível está tão longe removido da causa raiz.

É por isso que o princípio Fail Fast existe; economize a todos os envolvidos a dor de cabeça aplicando-a às suas APIs.

Mason Wheeler
fonte
7
Embora eu concorde com o princípio do que você está dizendo, acho que você errou no WRT com a intenção do Princípio da Robustez. Eu nunca vi isso com a intenção de significar "aceitar dados ruins", apenas "não ser excessivamente complicado sobre bons dados". Por exemplo, se a entrada for um arquivo CSV, o Princípio da robustez não seria um argumento válido para tentar analisar datas em um formato inesperado, mas suportaria um argumento de que inferir a ordem de colunas a partir de uma linha de cabeçalho seria uma boa idéia .
Morgen
9
@Orgen: o princípio de robustez foi usado para sugerir que os navegadores deveriam aceitar HTML bastante desleixado, e levou os sites implantados a serem muito mais desleixados do que teriam sido se os navegadores tivessem exigido HTML adequado. Uma grande parte do problema, porém, era o uso de um formato comum para conteúdo gerado por humanos e gerado por máquina, em oposição ao uso de formatos separados editáveis ​​por humanos e analisáveis ​​por máquina, juntamente com utilitários para conversão entre eles.
supercat 14/01
9
@ supercat: no entanto - ou apenas daqui - o HTML e a WWW foram extremamente bem-sucedidos ;-)
Doc Brown
11
@DocBrown: Muitas coisas realmente horríveis se tornaram padrões simplesmente porque foram a primeira abordagem disponível quando alguém com muita influência precisava adotar algo que atendesse a certos critérios mínimos e, quando eles ganharam força, já era tarde demais para selecionar algo melhor.
supercat 14/01
5
@supercat Exatamente. JavaScript imediatamente vem à mente, por exemplo ...
Mason Wheeler
13

Em geral, o código deve ser construído para manter, pelo menos, as seguintes restrições sempre que possível:

  1. Quando receber a entrada correta, produza a saída correta.

  2. Quando receber uma entrada válida (que pode ou não estar correta), produza uma saída válida (da mesma forma).

  3. Quando receber uma entrada inválida, processe-a sem efeitos colaterais além daqueles causados ​​pela entrada normal ou definidos como sinalização de erro.

Em muitas situações, os programas passam essencialmente por vários blocos de dados sem se preocupar particularmente se são válidos. Se esses pedaços contiverem dados inválidos, a saída do programa provavelmente conterá dados inválidos como conseqüência. A menos que um programa seja projetado especificamente para validar todos os dados e garantir que ele não produza saída inválida, mesmo quando recebida entrada inválida , os programas que processam sua saída devem permitir a possibilidade de dados inválidos dentro dele.

Embora a validação de dados no início seja frequentemente desejável, nem sempre é particularmente prático. Entre outras coisas, se a validade de um pedaço de dados depende do conteúdo de outros pedaços, e se a maioria dos dados alimentados em alguma sequência de etapas será filtrada ao longo do caminho, limitando a validação aos dados que o fazem passar todos os estágios podem gerar um desempenho muito melhor do que tentar validar tudo.

Além disso, mesmo que apenas se espere que um programa receba dados pré-validados, geralmente é bom que ele mantenha as restrições acima de qualquer maneira, sempre que possível. Repetir a validação completa em todas as etapas do processamento costuma ser uma grande perda de desempenho, mas a quantidade limitada de validação necessária para manter as restrições acima pode ser muito mais barata.

supercat
fonte
Então, tudo se resume a decidir se o resultado de uma chamada de API é uma "entrada".
mastov 16/01
@mastov: As respostas para muitas perguntas dependerão de como se define "entradas" e "comportamentos observáveis" / "saídas". Se o objetivo de um programa é processar números armazenados em um arquivo, sua entrada pode ser definida como a sequência de números (nesse caso, coisas que não são números não são possíveis) ou como um arquivo (nesse caso, qualquer coisa que aparecer em um arquivo seria uma entrada possível).
supercat 16/01
3

Vamos comparar os dois cenários e tentar chegar a uma conclusão.

Cenário 1 Nosso aplicativo assume que a API externa se comportará conforme o contrato.

Cenário 2 Nosso aplicativo assume que a API externa pode se comportar mal, portanto, adicione precauções.

Em geral, existe a chance de qualquer API ou software violar os contratos; pode ser devido a um bug ou a condições inesperadas. Mesmo uma API pode estar tendo problemas nos sistemas internos, resultando em resultados inesperados.

Se nosso programa for escrito, assumindo que a API externa cumprirá os contratos e evitará adicionar precauções; quem será a parte que enfrenta os problemas? Seremos nós, quem escreveu o código de integração.

Por exemplo, os valores nulos que você selecionou. Digamos, de acordo com o contrato da API, a resposta deve ter valores não nulos; mas se for subitamente violado, nosso programa resultará em NPEs.

Portanto, acredito que será melhor garantir que seu aplicativo tenha algum código adicional para lidar com cenários inesperados.

lkamal
fonte
1

Você sempre deve validar os dados recebidos - inseridos pelo usuário ou não - para ter um processo em andamento para lidar quando os dados recuperados dessa API externa forem inválidos.

De um modo geral, qualquer emenda em que os sistemas extra-organizacionais se encontrem deve exigir autenticação, autorização (se não definida simplesmente pela autenticação) e validação.

StarTrekRedneck
fonte
1

Em geral, sim, você deve sempre se proteger contra entradas defeituosas, mas, dependendo do tipo de API, "guarda" significa coisas diferentes.

Para uma API externa para um servidor, você não deseja criar acidentalmente um comando que trava ou comprometa o estado do servidor; portanto, você deve se proteger.

Para uma API como, por exemplo, uma classe de contêiner (lista, vetor, etc.), lançar exceções é um resultado perfeitamente adequado, comprometendo o estado da instância da classe pode ser aceitável até certo ponto (por exemplo, um contêiner classificado com um operador de comparação com falha não será ser classificada), mesmo travar o aplicativo pode ser aceitável, mas comprometer o estado do aplicativo - por exemplo, gravar em locais de memória aleatórios não relacionados à instância da classe - provavelmente não é.

Peter
fonte
0

Para dar uma opinião um pouco diferente: acho que pode ser aceitável trabalhar apenas com os dados fornecidos, mesmo que isso viole o contrato. Isso depende do uso: é algo que DEVE ser uma string para você, ou é algo que você está apenas exibindo / não usa etc. No último caso, basta aceitá-lo. Eu tenho uma API que só precisa de 1% dos dados entregues por outra API. Eu não poderia me importar menos com o tipo de dados nos 99%, por isso nunca vou verificá-los.

É preciso haver um equilíbrio entre "ter erros porque não verifico minhas entradas o suficiente" e "rejeito dados válidos porque sou muito rigoroso".

Christian Sauer
fonte
2
"Eu tenho uma API que precisa apenas de 1% dos dados entregues por outra API." Isso então abre a questão de por que sua API espera 100 vezes mais dados do que realmente precisa. Se você precisa armazenar dados opacos para transmitir, não precisa ser realmente específico sobre o que é e não precisa declará-los em nenhum formato específico; nesse caso, o chamador não violaria seu contrato .
Voo
11
@Voo - Minha suspeita é que eles estejam chamando alguma API externa (como "obter detalhes meteorológicos para a cidade X") e depois escolhendo os dados de que precisam ("temperatura atual") e ignorando o restante dos dados retornados ("precipitação" "," vento "," temperatura prevista "," frio do vento ", etc ...)
Stobor 16/01
@ChristianSauer - Eu acho que você não está tão longe do que é o consenso mais amplo - o 1% dos dados que você usa faz sentido para verificar, mas os 99% que você não usa não precisam necessariamente ser verificados. Você só precisa verificar as coisas que podem atrapalhar seu código.
Stobor 16/01
0

Minha opinião é sempre verificar sempre todas as entradas do meu sistema. Isso significa que todos os parâmetros retornados de uma API devem ser verificados, mesmo que meu programa não o utilize. Costumo verificar também todos os parâmetros enviados a uma API para verificar se estão corretos. Existem apenas duas exceções a esta regra, veja abaixo.

O motivo do teste é que, se por algum motivo a API / entrada estiver incorreta, meu programa não poderá confiar em nada. Talvez meu programa tenha sido vinculado a uma versão antiga da API que faça algo diferente do que eu acredito? Talvez meu programa tenha encontrado um bug no programa externo que nunca havia acontecido antes. Ou pior, acontece o tempo todo, mas ninguém liga! Talvez o programa externo esteja sendo enganado por um hacker para devolver coisas que podem prejudicar meu programa ou o sistema?

As duas exceções para testar tudo no meu mundo são:

  1. Desempenho após uma cuidadosa medição do desempenho:

    • nunca otimize antes de medir. O teste de todos os dados de entrada / retornados geralmente leva um tempo muito pequeno em comparação com a chamada real; portanto, sua remoção economiza pouco ou nada. Ainda manteria o código de detecção de erros, mas comentei, talvez por uma macro ou simplesmente comente.
  2. Quando você não tem idéia do que fazer com um erro

    • há momentos, muitas vezes, em que seu design simplesmente não permite o tratamento do tipo de erro que você encontraria. Talvez o que você deva fazer seja registrar um erro, mas não há nenhum registro de erro no sistema. Quase sempre é possível encontrar uma maneira de "lembrar" o erro, permitindo que pelo menos você, como desenvolvedor, verifique mais tarde. Os contadores de erros são uma coisa boa a se ter em um sistema, mesmo se você optar por não ter log.

Exatamente com que cuidado verificar as entradas / valores de retorno é uma questão importante. Por exemplo, se a API retornar uma string, eu verificaria se:

  • o tipo de dados realmente é uma sequência

  • e esse comprimento está entre os valores mínimo e máximo. Sempre verifique as strings quanto ao tamanho máximo que meu programa pode esperar (retornar strings muito grandes é um problema de segurança clássico em sistemas em rede).

  • Algumas seqüências de caracteres devem ser verificadas quanto a caracteres ou conteúdo "ilegais" quando isso for relevante. Se o seu programa enviar a string para dizer um banco de dados posteriormente, é uma boa ideia verificar se há ataques no banco de dados (procure por injeção de SQL). É melhor fazer esses testes nas fronteiras do meu sistema, onde posso identificar de onde veio o ataque e falhar cedo. A execução de um teste completo de injeção de SQL pode ser difícil quando as cadeias são combinadas posteriormente, para que o teste seja realizado antes de chamar o banco de dados, mas se você encontrar alguns problemas mais cedo, poderá ser útil.

O motivo para testar os parâmetros que eu envio à API é garantir que eu receba de volta um resultado correto. Novamente, fazer esses testes antes de chamar uma API pode parecer desnecessário, mas requer muito pouco desempenho e pode detectar erros no meu programa. Portanto, os testes são mais valiosos no desenvolvimento de um sistema (mas hoje em dia todo sistema parece estar em desenvolvimento contínuo). Dependendo dos parâmetros, os testes podem ser mais ou menos completos, mas eu costumo achar que você pode definir valores mínimos e máximos permitidos na maioria dos parâmetros que meu programa poderia criar. Talvez uma string sempre deva ter pelo menos 2 caracteres e ter no máximo 2000 caracteres? O mínimo e o máximo devem estar dentro do que a API permite, pois sei que meu programa nunca usará toda a gama de alguns parâmetros.

ghellquist
fonte