Supposons que vous codiez une fonction qui prend les entrées d'une API externe MyAPI
.
Cette API externe MyAPI
a un contrat qui stipule qu'elle renverra un string
ou un number
.
Est-il recommandé de se prémunir contre des choses comme null
, undefined
, boolean
, etc. même si cela ne fait pas partie de l'API de MyAPI
? En particulier, puisque vous n'avez aucun contrôle sur cette API, vous ne pouvez pas faire la garantie par quelque chose comme l'analyse de type statique, il vaut donc mieux être prudent que désolé?
Je pense par rapport au principe de robustesse .
Vous ne devez jamais faire confiance aux entrées de votre logiciel, quelle que soit la source. Non seulement la validation des types est importante, mais également les plages d'entrée et la logique métier. Par un commentaire, cela est bien décrit par OWASP
Ne pas le faire vous laissera au mieux avec des données inutiles que vous devrez nettoyer plus tard, mais au pire, vous laisserez une opportunité d'exploits malveillants si ce service en amont est compromis d'une manière ou d'une autre (par exemple le piratage de Target). La gamme de problèmes entre les deux 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 d'expansion.
Par "ne jamais faire confiance aux entrées", je veux simplement dire que vous ne pouvez pas supposer que vous recevrez toujours des informations valides et dignes de confiance de systèmes en amont ou en aval, et donc vous devriez toujours aseptiser cette entrée au mieux de vos capacités, ou rejeter il.
Un argument est apparu dans les commentaires que j'aborderai à 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 dépend de vous, et dépend beaucoup de la fonction commerciale réelle que vous essayez d'accomplir, mais au minimum vous l'enregistrez pour un débogage ultérieur et sinon vous vous assurez que votre application ne va pas dans un état irrécupérable ou peu sûr.
Bien que vous ne puissiez jamais connaître toutes les entrées possibles que quelqu'un/quelque chose pourrait vous donner, vous pouvez certainement limiter ce qui est autorisé en fonction des exigences de l'entreprise et effectuer une certaine forme de liste blanche d'entrée en fonction de cela.
Oui, bien sûr. 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 retournerait pas ce que dit le contrat, n'est-ce pas? Donc, au moins, vous devez faire face à un tel comportement d'une manière ou d'une autre . Une forme minimale de gestion des erreurs vaut toujours l'effort (très minime!), Et il n'y a absolument aucune excuse pour ne pas implémenter quelque chose comme ça.
Cependant, combien d'efforts vous devez investir pour traiter un tel cas dépend fortement du cas et ne peut être répondu que dans le contexte de votre système. Souvent, une courte entrée de journal et laisser l'application se terminer correctement peut suffire. Parfois, vous serez mieux placé pour implémenter une gestion détaillée des exceptions, traitant de différentes formes de "mauvaises" valeurs de retour, et peut-être devrez-vous implémenter 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, à utiliser par moins de 10 personnes et où l'impact financier d'un crash d'application est assez faible, ou si vous créez une nouvelle voiture autonome au volant système, où un crash d'application peut coûter des vies.
Il n'y a donc pas de raccourci pour réfléchir à ce que vous faites , utiliser votre bon sens est toujours obligatoire.
Le principe de robustesse - en particulier, la "être libéral dans ce que vous acceptez" moitié - 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 incorrecte ou autrement incorrecte, vous avez deux choix. Vous pouvez soit le rejeter (de préférence avec une explication de ce qui ne va pas), soit essayer de comprendre ce que cela était censé signifier.
EDIT: Il s'avère 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 RFC 1958 . Il est dit:
3.9 Soyez strict lors de l'envoi et tolérant lors de la réception. Les implémentations doivent suivre les spécifications précisément lors de l'envoi au réseau et tolérer les entrées défectueuses du réseau. En cas de doute, supprimez silencieusement l'entrée défectueuse, sans renvoyer de message d'erreur, sauf si cela est requis par la spécification.
C'est tout simplement faux du début à la fin. Il est difficile de concevoir une notion plus erronée de la gestion des erreurs que de "rejeter silencieusement une entrée défectueuse sans renvoyer un message d'erreur", pour les raisons données dans cet article.
Voir également le document de l'IETF Les conséquences néfastes du principe de robustesse pour plus de détails sur ce point.
Jamais, jamais, jamais choisissez cette deuxième option, sauf si vous avez des ressources équivalentes à l'équipe de recherche de Google pour lancer votre projet , car c'est ce qu'il faut pour trouver un programme informatique qui fasse quelque chose de proche d'un travail décent dans ce domaine de problème particulier. (Et même alors, les suggestions de Google donnent l'impression de sortir tout droit du champ gauche environ la moitié du temps.) Si vous essayez de le faire, vous vous retrouverez avec un énorme mal de tête que votre programme essaiera fréquemment 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. L'évident est parce qu'alors vous avez de mauvaises données dans votre système. Le moins évident est que dans de nombreux cas, ni vous ni l'expéditeur ne vous rendrez compte que quelque chose s'est mal passé bien plus tard sur la route lorsque quelque chose vous explose au visage, puis tout à coup, vous avez un gros gâchis coûteux à réparer et aucune idée ce qui a mal tourné parce que l'effet perceptible est si loin de la cause profonde.
C'est pourquoi le principe Fail Fast existe; sauvez tous les maux de tête impliqués en l'appliquant à vos API.
En général, le code doit être construit pour respecter au moins les contraintes suivantes lorsque cela est possible:
Lorsqu'une entrée correcte est fournie, produire une sortie correcte.
Lorsqu'une entrée valide est donnée (qui peut ou non être correcte), produire une sortie valide (de même).
Lorsqu'une entrée invalide est donnée, traitez-la sans aucun effet secondaire au-delà de ceux causés par une entrée normale ou ceux qui sont définis comme signalant une erreur.
Dans de nombreuses situations, les programmes passent essentiellement par différents blocs de données sans se soucier particulièrement de leur validité. Si de tels morceaux contiennent des données invalides, la sortie du programme contiendra probablement des données invalides en conséquence. À 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 lorsqu'il reçoit une entrée invalide, les programmes qui traitent sa sortie devraient permettre la possibilité de données invalides en son sein.
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 blocs, et si la majorité des données introduites dans une séquence d'étapes seront filtrées en cours de route, limitant la validation aux données qui le font à travers toutes les étapes peuvent donner des performances bien meilleures 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 faire respecter les contraintes ci-dessus de toute façon chaque fois que cela est possible. La répétition de la validation complète à chaque étape du traitement serait souvent une perte de performances importante, mais le nombre limité de validations nécessaires pour respecter les contraintes ci-dessus peut être beaucoup moins cher.
Comparons les deux scénarios et essayons de conclure.
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, donc ajoutez des précautions.
En général, il existe une possibilité pour toute API ou logiciel de violer les accords; peut être dû à un bogue ou à des conditions inattendues. Même une API peut avoir des problèmes dans les systèmes internes entraînant des résultats inattendus.
Si notre programme est écrit en supposant que l'API externe respectera les accords et évitera d'ajouter des précautions; qui sera le parti face aux problèmes? Ce sera nous, ceux qui ont écrit le code d'intégration.
Par exemple, les valeurs nulles que vous avez choisies. Disons que, conformément à l'accord d'API, la réponse doit avoir des valeurs non nulles; mais s'il est soudainement violé, notre programme entraînera des NPE.
Donc, je pense qu'il vaudra mieux s'assurer que votre application dispose d'un code supplémentaire pour répondre aux scénarios inattendus.
En général, oui, vous devez toujours vous prémunir contre les entrées défectueuses, mais selon le type d'API, "garde" signifie des choses différentes.
Pour une API externe à un serveur, vous ne voulez pas créer accidentellement une commande qui plante ou compromet l'état du serveur, vous devez donc vous prémunir contre cela.
Pour une API comme par exemple une classe de conteneur (liste, vecteur, etc.), lever des exceptions est un résultat parfaitement correct, compromettre l'état de l'instance de classe peut être acceptable dans une certaine mesure (par exemple, un conteneur trié fourni avec un opérateur de comparaison défectueux ne sera pas trié), même planter l'application peut être acceptable, mais compromettre l'état de l'application - par exemple l'écriture dans des emplacements de mémoire aléatoires sans rapport avec l'instance de classe - n'est probablement pas.
Vous devez toujours valider les données entrantes - saisies par l'utilisateur ou non - afin d'avoir un processus en place pour gérer lorsque les données récupérées à partir de cette API externe ne sont pas valides.
De manière générale, toute couture où se rencontrent des systèmes extra-organisationnels doit exiger une authentification, une autorisation (si elle n'est pas définie simplement par authentification) et une validation.
Pour donner un avis légèrement différent: je pense qu'il peut être acceptable de simplement travailler avec les données qui vous sont fournies, même si cela viole 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 affichez/n'utilisez pas, etc. Dans ce dernier cas, acceptez-le simplement. J'ai une API qui n'a besoin que de 1% des données fournies par une autre API. Je me fiche du type de données dans le 99%, donc je ne le 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 rejette des données valides parce que je suis trop strict".
Ma vision 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 également 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/l'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 du programme externe qui ne s'est jamais produit auparavant. Ou pire encore, ça arrive tout le temps mais personne ne s'en soucie! Peut-être que le programme externe est dupé par un pirate informatique pour renvoyer des choses qui peuvent nuire à mon programme ou au système?
Les deux exceptions à tout tester dans mon monde sont:
Performances après une mesure minutieuse des performances:
Lorsque vous ne savez pas quoi faire avec une erreur
La précision de la vérification des entrées/valeurs de retour est une question importante. Par exemple, si l'API renvoie une chaîne, je vérifierais que:
le type de données est effectivement 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 ou le contenu "illégaux" lorsque cela est pertinent. Si votre programme peut envoyer la chaîne pour dire une base de données plus tard, c'est une bonne idée de vérifier les attaques de base de données (recherche d'injection SQL). Ces tests sont mieux effectués aux frontières de mon système, où je peux déterminer d'où vient l'attaque et je peux échouer tôt. Faire un test d'injection SQL complet peut être difficile lorsque des chaînes sont combinées ultérieurement, de sorte que le test doit être effectué avant d'appeler la base de données, mais si vous pouvez trouver des problèmes au début, cela peut être utile.
La raison du test des paramètres que j'envoie à l'API est d'être sûr d'obtenir un résultat correct. Encore une fois, faire ces tests avant d'appeler une API peut sembler inutile mais cela prend très peu de performances et peut attraper des erreurs dans mon programme. Par conséquent, les tests sont les plus utiles lors du développement d'un système (mais de nos jours, chaque système semble être en développement continu). Selon les paramètres, les tests peuvent être plus ou moins approfondis mais j'ai tendance à trouver que vous pouvez souvent définir des valeurs min et max autorisées sur la plupart des paramètres que mon programme pourrait créer. Peut-être qu'une chaîne doit toujours avoir au moins 2 caractères et être au maximum de 2000 caractères? Le minimum et le maximum devraient être à l'intérieur de ce que l'API permet car je sais que mon programme n'utilisera jamais la gamme complète de certains paramètres.