Devriez-vous vous protéger contre les valeurs inattendues des API externes?

51

Disons que vous codez une fonction qui prend l’entrée d’une API externe MyAPI.

Cette API externe MyAPIa un contrat qui stipule qu’elle renverra un stringou number.

Est - il recommandé de se prémunir contre des choses comme null, undefined, boolean, etc. , même si elle ne fait pas partie de l'API MyAPI? En particulier, étant donné que vous n'avez aucun contrôle sur cette API, vous ne pouvez pas donner la garantie par une analyse du type statique, il est donc préférable d'être prudent que de s'excuser.

Je pense au principe de robustesse .

Adam Thompson
la source
16
Quels sont les effets de ne pas gérer ces valeurs inattendues si elles sont renvoyées? Pouvez-vous vivre avec ces impacts? Vaut-il la peine de gérer ces valeurs inattendues pour éviter de devoir faire face aux impacts?
Vincent Savard
55
Si vous les attendez, alors, par définition, ils ne sont pas inattendus.
Mason Wheeler le
28
N'oubliez pas que l'API n'est pas obligée de vous restituer uniquement du JSON valide (je suppose que c'est du JSON). Vous pouvez également obtenir une réponse comme celle-ci<!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
5
Que signifie "API externe"? Est-ce toujours sous votre contrôle?
Déduplicateur
11
"Un bon programmeur est quelqu'un qui regarde dans les deux sens avant de traverser une rue à sens unique."
jeroen_de_schutter le

Réponses:

103

Vous ne devriez jamais faire confiance aux entrées de votre logiciel, quelle que soit sa source. Non seulement la validation des types est importante, mais également la plage des entrées et la logique métier. Par un commentaire, ceci est bien décrit par OWASP

Dans le meilleur des cas, vous conserverez au mieux des données erronées que vous devrez nettoyer plus tard, mais au pire, vous laisserez une possibilité d'exploits malveillants si ce service en amont est compromis d'une manière ou d'une autre (qv the hack Target). L’éventail des problèmes entre ces opérations inclut l’obtention de votre application dans un état irrécupérable.


D'après les commentaires, je peux voir que ma réponse pourrait peut-être utiliser un peu de développement.

Par "ne faites jamais confiance aux entrées", je veux simplement dire que vous ne pouvez pas présumer que vous recevrez toujours des informations valides et dignes de confiance des systèmes en amont ou en aval. Par conséquent, vous devez toujours optimiser ces entrées, ou les rejeter. il.

Un argument est apparu dans les commentaires que je vais aborder à titre d'exemple. Bien que oui, vous devez faire confiance à votre système d'exploitation dans une certaine mesure, il n'est pas déraisonnable, par exemple, de rejeter les résultats d'un générateur de nombres aléatoires si vous lui demandez un nombre compris entre 1 et 10 et qu'il répond par "bob".

De même, dans le cas de l'OP, vous devez absolument vous assurer que votre application n'accepte que des entrées valides du service en amont. Ce que vous faites quand ce n'est pas OK est à vous de décider et dépend en grande partie de la fonction commerciale que vous essayez d'accomplir, mais vous devez le consigner au minimum pour un débogage ultérieur et vous assurer que votre application ne fonctionne pas. dans un état irrécupérable ou précaire.

Bien que vous ne puissiez jamais connaître toutes les entrées possibles que quelqu'un ou quelque chose puisse vous donner, vous pouvez certainement limiter ce qui est permis en fonction des besoins de l'entreprise et créer une sorte de liste blanche d'entrée basée sur ces informations.

Paul
la source
20
Que signifie qv?
JonH
15
@JonH essentiellement "voir aussi" ... le hack cible est un exemple qu'il référence en.oxforddictionaries.com/definition/qv .
Andrewtweber
8
Cette réponse est telle quelle, elle n'a tout simplement aucun sens. Il est impossible de prévoir toutes les façons dont une bibliothèque tierce pourrait se comporter mal. Si la documentation d'une fonction de bibliothèque assure explicitement que le résultat aura toujours certaines propriétés, vous devriez pouvoir vous y fier, ce que les concepteurs ont assuré que cette propriété sera réellement conservée. C'est leur responsabilité de disposer d'une suite de tests qui vérifie ce genre de choses et de soumettre un correctif au cas où une situation se produirait. Vous vérifier ces propriétés dans votre propre code viole le principe DRY.
gauche du
23
@leftaroundabout no, mais vous devriez être capable de prédire toutes les choses valides que votre application peut accepter et de rejeter le reste.
Paul
10
@leftaroundabout Il ne s'agit pas de tout méfier, mais bien de se méfier des sources externes non fiables. Tout est question de modélisation de la menace. Si vous ne l'avez pas déjà fait, votre logiciel n'est pas sécurisé (comment peut-il l'être, si vous n'avez même jamais réfléchi à quel type d'acteurs et de menaces vous souhaitez sécuriser votre application?). Pour un logiciel d'entreprise courant, il est raisonnable de penser que les appelants peuvent être malveillants, alors qu'il est rarement raisonnable de penser que votre système d'exploitation est une menace.
Voo le
33

Oui bien sur. Mais qu'est-ce qui vous fait penser que la réponse pourrait être différente?

Vous ne voulez sûrement pas laisser votre programme se comporter de manière imprévisible au cas où l'API ne renverrait pas ce que le contrat dit, n'est-ce pas? Donc , au moins vous avez à traiter avec le comportement d'une telle façon ou d' autre . Une forme minimale de traitement des erreurs vaut toujours l'effort (très minime!), Et il n'y a absolument aucune excuse pour ne pas mettre en œuvre quelque chose comme ça.

Toutefois, les efforts que vous devez déployer pour traiter un tel cas dépendent fortement de votre cas et ne peuvent être résolus que dans le contexte de votre système. Souvent, une courte entrée dans le journal et laisser l’application se terminer normalement peuvent suffire. Il est parfois préférable de mettre en œuvre une gestion détaillée des exceptions, de traiter différentes formes de valeurs de retour «erronées» et éventuellement de mettre en œuvre une stratégie de secours.

Mais cela fait une énorme différence si vous écrivez juste une application de formatage de feuille de calcul interne, devant être utilisée par moins de 10 personnes et où l'impact financier d'un blocage d'application est assez faible, ou si vous créez une nouvelle conduite autonome. système, où une panne d'application peut coûter des vies.

Il n’ya donc pas de raccourci pour réfléchir à ce que vous faites , il est toujours impératif d’utiliser votre bon sens.

Doc Brown
la source
Que faire est une autre décision. Vous pouvez avoir une solution de basculement. Tout ce qui est asynchrone peut être réessayé avant de créer un journal des exceptions (ou une lettre morte). Une alerte active auprès du fournisseur ou du fournisseur peut être une option si le problème persiste.
mckenzm
@mckenzm: le fait que le PO pose une question à propos de laquelle la réponse littérale ne peut évidemment être que "oui" est un signe IMHO qu'il peut ne pas être intéressé par une réponse littérale. Il semble qu'ils demandent "est-il nécessaire de se prémunir contre différentes formes de valeurs inattendues d'une API et de les traiter différemment" ?
Doc Brown le
1
hmm, la merde / carpe / approche approche. Est-ce notre faute si nous passons de mauvaises demandes (mais légales)? La réponse est-elle possible, mais non utilisable pour nous en particulier? ou la réponse est-elle corrompue? Différents scénarios, cela ressemble maintenant à un devoir.
mckenzm
21

Le principe de robustesse - en particulier la moitié "soyez libéraux dans ce que vous acceptez" - est une très mauvaise idée dans le logiciel. Il a été développé à l'origine dans le contexte du matériel, où les contraintes physiques rendent les tolérances d'ingénierie très importantes, mais dans le logiciel, lorsque quelqu'un vous envoie une entrée mal formée ou incorrecte, vous avez le choix. Vous pouvez soit le rejeter (de préférence avec une explication de ce qui ne va pas), soit vous pouvez essayer de comprendre ce que cela était supposé vouloir dire.

EDIT: Il se trouve que je me suis trompé dans la déclaration ci-dessus. Le principe de robustesse ne vient pas du monde du matériel, mais de l'architecture Internet, en particulier du RFC 1958 . Il est dit:

3.9 Soyez strict lors de l'envoi et tolérant lors de la réception. Les mises en œuvre doivent suivre les spécifications précisément lors de l'envoi au réseau et tolérer une entrée erronée du réseau. En cas de doute, supprimez silencieusement les entrées défectueuses, sans renvoyer de message d'erreur, sauf si cela est requis par la spécification.

C’est, tout simplement, tout simplement faux du début à la fin. Il est difficile de concevoir une notion plus erronée de traitement des erreurs que "supprimer une entrée défectueuse en silence sans renvoyer un message d'erreur", pour les raisons données dans cet article.

Voir également le document de l'IETF intitulé Les conséquences néfastes du principe de robustesse pour plus de précisions sur ce point.

Jamais, jamais, jamais, ne choisissez cette deuxième option que si vous disposez de ressources équivalentes à celles de l'équipe de recherche de Google, car c'est ce qu'il faut pour créer un programme informatique qui fasse tout ce qui est proche d'un travail décent dans ce domaine problématique. (Et même dans ce cas, les suggestions de Google donnent l'impression qu'elles sortent du champ gauche environ la moitié du temps.) Si vous essayez de le faire, vous allez vous retrouver avec un mal de tête énorme où votre programme tentera souvent d'interpréter mauvaise entrée en tant que X, alors que l'expéditeur voulait vraiment dire Y.

C'est mauvais pour deux raisons. La plus évidente est parce que vous avez alors de mauvaises données dans votre système. La moins évidente est que dans de nombreux cas, ni vous ni l'expéditeur ne réaliserez que quelque chose ne va pas bien avant que beaucoup plus tard ne se produise, quand quelque chose explose à la figure, puis vous avez soudainement un gros désordre coûteux à réparer et aucune idée ce qui a mal tourné parce que l’effet notable est si loin de la cause première.

C'est pourquoi le principe Fail Fast existe; épargnez le mal de tête à toutes les personnes concernées en l'appliquant à vos API.

Maçon Wheeler
la source
7
Bien que je sois d’accord avec le principe de ce que vous dites, je pense que vous confondez l’objectif du principe de robustesse. Je ne l'ai jamais vu dans le but de signifier "accepter de mauvaises données", mais seulement "ne soyez pas trop dur à propos de bonnes données". Par exemple, si l'entrée est un fichier CSV, le principe de robustesse ne constitue pas un argument valable pour essayer d'analyser les dates dans un format inattendu, mais prend en charge un argument selon lequel la déduction de l'ordre des colonnes à partir d'une ligne d'en-tête serait une bonne idée. .
Morgen
9
@Morgen: Le principe de robustesse a été utilisé pour suggérer que les navigateurs devraient accepter du HTML plutôt bâclé et a conduit à des sites Web déployés beaucoup plus bâclés qu'ils ne l'auraient été si les navigateurs avaient exigé un code HTML correct. Une grande partie du problème, cependant, résidait dans l’utilisation d’un format commun pour le contenu généré par l’homme et par un ordinateur, par opposition à l’utilisation de formats distincts modifiables par l’homme et analysables par la machine, ainsi que d’utilitaires de conversion entre eux.
Supercat
9
@supercat: néanmoins - ou tout simplement - HTML et le WWW ont été extrêmement fructueux ;-)
Doc Brown
11
@DocBrown: Beaucoup de choses vraiment horribles sont devenues des normes simplement parce qu'elles étaient la première approche disponible quand une personne ayant beaucoup d'influence devait adopter quelque chose qui répondait à certains critères minimaux, et au moment où elle gagnait du terrain trop tard pour choisir quelque chose de mieux.
Supercat
5
@supercat Exactement. JavaScript me vient immédiatement à l'esprit, par exemple ...
Mason Wheeler
13

En général, un code devrait être construit pour respecter au moins les contraintes suivantes chaque fois que cela est possible:

  1. Lorsque l'entrée est correcte, produisez une sortie correcte.

  2. Lorsqu'une entrée valide est donnée (que cela soit correct ou non), produit une sortie valide (de même).

  3. Lorsque l'entrée est invalide, traitez-la sans aucun effet secondaire autre que ceux causés par une entrée normale ou ceux définis comme signalant une erreur.

Dans de nombreuses situations, les programmes vont essentiellement passer par différents morceaux de données sans se soucier de leur validité. Si de tels morceaux contiennent des données non valides, la sortie du programme contiendra probablement des données non valides. À moins qu'un programme ne soit spécifiquement conçu pour valider toutes les données et garantir qu'il ne produira pas de sortie invalide même si une entrée invalide lui est fournie , les programmes qui traitent sa sortie devraient permettre la possibilité que des données invalides s'y trouvent.

Bien que la validation précoce des données soit souvent souhaitable, elle n’est pas toujours particulièrement pratique. Entre autres choses, si la validité d’un bloc de données dépend du contenu d’autres morceaux et si la majorité des données introduites dans une séquence d’étapes seront filtrées en cours de route, limitant ainsi la validation aux données qui le permettent. toutes les étapes peuvent donner de bien meilleures performances que d'essayer de tout valider.

De plus, même si un programme ne doit recevoir que des données pré-validées, il est souvent bon de le laisser respecter les contraintes ci-dessus de toute façon, chaque fois que possible. Répéter la validation complète à chaque étape du traitement serait souvent une perte de performances majeure, mais la quantité limitée de validation nécessaire pour respecter les contraintes ci-dessus peut coûter beaucoup moins cher.

supercat
la source
Ensuite, il s’agit de décider si le résultat d’un appel API est une "entrée".
Mastov
@mastov: Les réponses à de nombreuses questions dépendront de la définition des "entrées" et des "comportements observables" / "sorties". Si le programme a pour but de traiter les numéros stockés dans un fichier, son entrée peut être définie en tant que séquence de nombres (dans ce cas, les entrées non numériques ne sont pas des entrées possibles), ou en tant que fichier (dans apparaître dans un fichier serait une entrée possible).
Supercat
3

Comparons les deux scénarios et essayons de tirer une conclusion.

Scénario 1 Notre application suppose que l'API externe se comportera conformément à l'accord.

Scénario 2 Notre application suppose que l'API externe peut mal se comporter, d'où l'ajout de précautions.

En général, une API ou un logiciel peut enfreindre les contrats. peut être dû à un bogue ou à des conditions inattendues. Même une API peut avoir des problèmes dans les systèmes internes, ce qui entraîne des résultats inattendus.

Si notre programme est écrit en supposant que l’API externe respecte les accords et évite d’ajouter des précautions; qui sera la partie face aux problèmes? Ce sera nous, ceux qui ont écrit le code d'intégration.

Par exemple, les valeurs NULL que vous avez choisies. Selon l'accord de l'API, la réponse doit avoir des valeurs non nulles. mais s'il est violé soudainement, notre programme produira des NPE.

Je pense donc qu'il sera préférable de s'assurer que votre application dispose d'un code supplémentaire pour traiter les scénarios inattendus.

lkamal
la source
1

Vous devez toujours valider les données entrantes - saisies par l'utilisateur ou autre - afin de disposer d'un processus permettant de gérer le moment où les données extraites de cette API externe sont invalides.

De manière générale, toute couture où se rencontrent des systèmes extra-organisationnels doit nécessiter une authentification, une autorisation (si elle n'est pas simplement définie par authentification) et une validation.

StarTrekRedneck
la source
1

En général, oui, vous devez toujours vous prémunir contre des entrées erronées, mais selon le type d'API, «garde» signifie différentes choses.

Pour une API externe à un serveur, vous ne souhaitez pas créer accidentellement une commande qui bloque ou compromet l'état du serveur. Vous devez donc vous protéger contre cela.

Pour une API telle que, par exemple, une classe de conteneur (liste, vecteur, etc.), le lancement d’exceptions est un résultat parfait, compromettre l’état de l’instance de la classe peut être acceptable dans une certaine mesure (par exemple, un conteneur trié doté d’un opérateur de comparaison défaillant ne trié), même planter l’application peut être acceptable, mais compromettre l’état de l’application - par exemple écrire dans des emplacements de mémoire aléatoires non liés à l’instance de la classe - n’est fort probablement pas.

Peter
la source
0

Pour donner un avis légèrement différent: je pense qu’il est acceptable de travailler uniquement avec les données qui vous sont fournies, même si cela ne respecte pas son contrat. Cela dépend de l'utilisation: c'est quelque chose qui DOIT être une chaîne pour vous, ou est-ce quelque chose que vous êtes en train d'afficher / que vous n'utilisez pas, etc. Dans ce dernier cas, acceptez-le simplement. J'ai une API qui nécessite seulement 1% des données fournies par une autre API. Je me fiche de savoir quel genre de données sont dans les 99%, donc je ne les vérifierai jamais.

Il doit y avoir un équilibre entre "avoir des erreurs parce que je ne vérifie pas assez mes entrées" et "je refuse les données valides parce que je suis trop strict".

Christian Sauer
la source
2
"J'ai une API qui nécessite seulement 1% des données fournies par une autre API." Cela pose alors la question de savoir pourquoi votre API attend 100 fois plus de données que ce dont elle a réellement besoin. Si vous devez stocker des données opaques pour les transmettre, vous n'avez pas vraiment besoin de préciser leur nature ni de les déclarer dans un format spécifique. Dans ce cas, l'appelant n'enfreindrait pas votre contrat. .
Voo le
1
@Voo - Je soupçonne qu'ils appellent une API externe (comme "obtenir les détails météo pour la ville X"), puis sélectionnent les données dont ils ont besoin ("température actuelle") et ignorent le reste des données renvoyées ("précipitations". "," vent "," prévisions de température "," refroidissement éolien ", etc ...)
Stobor
@ChristianSauer - Je pense que vous n'êtes pas si loin de ce que le consensus général est - le 1% des données que vous utilisez a du sens pour vérifier, mais les 99% que vous n'avez pas besoin de vérifier. Il vous suffit de vérifier les choses qui pourraient faire trébucher votre code.
Stobor
0

Mon point de vue est de toujours, toujours vérifier chaque entrée de mon système. Cela signifie que chaque paramètre renvoyé par une API doit être vérifié, même si mon programme ne l'utilise pas. J'ai aussi tendance à vérifier l'exactitude de chaque paramètre que j'envoie à une API. Il n'y a que deux exceptions à cette règle, voir ci-dessous.

La raison du test est que si, pour une raison quelconque, l’API / entrée est incorrecte, mon programme ne peut s’appuyer sur rien. Peut-être que mon programme était lié à une ancienne version de l'API qui fait quelque chose de différent de ce que je crois? Peut-être que mon programme est tombé sur un bogue dans le programme externe qui n’était jamais arrivé auparavant. Ou pire encore, cela arrive tout le temps mais personne ne s'en soucie! Peut-être que le programme externe se laisse berner par un pirate informatique pour renvoyer des éléments susceptibles de nuire à mon programme ou au système?

Les deux exceptions à tout tester dans mon monde sont:

  1. Performance après mesure minutieuse de la performance:

    • n'optimisez jamais avant d'avoir mesuré. Le test de toutes les données entrées / renvoyées prend le plus souvent très peu de temps par rapport à l’appel, aussi sa suppression permet souvent d’économiser peu ou rien. Je garderais toujours le code de détection d'erreur, mais le commenterais, peut-être par une macro ou simplement en le commentant.
  2. Quand vous ne savez pas quoi faire avec une erreur

    • Il arrive parfois que votre conception ne permette tout simplement pas de gérer le type d'erreur que vous rencontriez. Peut-être devriez-vous consigner une erreur, mais il n'y a pas de consignation d'erreur dans le système. Il est presque toujours possible de trouver un moyen de "se souvenir" de l'erreur, ce qui permet au moins à vous, développeur, de la vérifier ultérieurement. Les compteurs d'erreurs sont une bonne chose à avoir dans un système, même si vous choisissez de ne pas vous connecter.

Exactement, comment vérifier avec soin les entrées / valeurs renvoyées est une question importante. Par exemple, si l’API est supposée renvoyer une chaîne, je vérifierais que:

  • le type de données actully est une chaîne

  • et cette longueur est comprise entre les valeurs min et max. Vérifiez toujours la taille maximale des chaînes que mon programme peut s'attendre à gérer (le renvoi de chaînes trop grandes est un problème de sécurité classique dans les systèmes en réseau).

  • Certaines chaînes doivent être vérifiées pour les caractères "illégaux" ou le contenu lorsque cela est pertinent. Si votre programme peut envoyer la chaîne pour dire une base de données ultérieurement, il est judicieux de vérifier les attaques de base de données (recherche d'injection SQL). Ces tests s’effectuent mieux aux frontières de mon système, où je peux localiser l’attaque et où je peux échouer plus tôt. Effectuer un test d'injection SQL complet peut s'avérer difficile lorsque les chaînes sont combinées ultérieurement. Ce test doit donc être effectué avant d'appeler la base de données. Toutefois, si vous pouvez détecter certains problèmes plus tôt, cela peut s'avérer utile.

Les paramètres de test que j’envoie à l’API sont testés pour que le résultat obtenu soit correct. Encore une fois, effectuer ces tests avant d'appeler une API peut sembler inutile, mais cela prend très peu de performances et risque de contenir des erreurs dans mon programme. Par conséquent, les tests sont plus précieux lors du développement d’un système (mais à présent, tous les systèmes semblent être en développement continu). Selon les paramètres, les tests peuvent être plus ou moins approfondis, mais j’ai tendance à penser que vous pouvez souvent définir des valeurs minimales et maximales autorisées pour la plupart des paramètres que mon programme pourrait créer. Peut-être qu'une chaîne devrait toujours avoir au moins 2 caractères et une longueur maximale de 2000 caractères? Les valeurs min et maximum doivent correspondre à celles permises par l'API, car je sais que mon programme n'utilisera jamais toute la gamme de certains paramètres.

ghellquist
la source