let readQuery (source:Datasource) : string = ...
let someLogic (x:string) : Results = ...
someLogic (readQuery …)
Nous n'avons donc aucune protection contre l'utilisation de chaînes non assainies. Par exemple, si readQuery
renvoie une chaîne de caractères contenant du code malveillant, someLogic
l'utiliserait, avec un effet désastreux. Nous voulons nous assurer que someLogic
ne puisse pas utiliser une chaîne de caractères non nettoyée. C'est un travail pour les types, mais QUELS types ? Par exemple, que se passerait-il si nous enveloppions notre chaîne dans un type et si nous avions un drapeau d'assainissement, ainsi que le code approprié, comme dans le cas suivant ?
type ExternalString = {
value: string;
isSanitized: bool;
}
let sanitize (x:string) : ExternalString = {
value = …;
isSanitized = true;
}
let readQuery (source:Datasource) : string = …
let someLogic (x:ExternalString) : Results = …
let results = someLogic (sanitize (readQuery …))
Cette approche nous oblige à ajouter de la logique à someLogic
pour vérifier l'indicateur d'assainissement avant d'utiliser la chaîne. C'est un travail supplémentaire et nous pouvons oublier de le faire. Cependant, que se passerait-il si, au lieu d'utiliser un drapeau, nous faisions des chaînes assainies un type différent ?
type SanitizedString = Sanitized of string
let readQuery (source:Datasource) : string = …
let sanitize (x:string) : SanitizedString = Sanitized (...)
let someLogic (x:SanitizedString) : Results = …
let results = someLogic (sanitize (readQuery …))
Aujourd'hui, la fonction someLogic
n'accepte que le type Chaîne assainie
. Et puisque la seule façon de l'obtenir est d'appeler sanitize
il faut donc assainir la chaîne avant de l'utiliser.
Plutôt que de mettre en place un drapeau pour indiquer que la chaîne a été assainie, nous nous assurons que les chaînes assainies sont d'un type entièrement différent. L'idée clé est qu'un drapeau marque souvent les données comme étant d'un type différent. Alors pourquoi ne pas simplement créer un nouveau type ?
Cela permet d'éviter les cas où l'on oublie d'appeler l'assainissement des chaînes de caractères. De plus, en n'exigeant pas que nous vérifiions un drapeau d'assainissement dans notre logique, cela élimine les erreurs résultant d'un oubli. Ce n'est pas à l'épreuve des balles, mais il y a moins de possibilités de se mettre en difficulté.
Nous n'avons pas besoin de types de données algébriques pour cela. Nous pourrions déclarer un nouvel enregistrement, une nouvelle classe, etc... en tant qu'enveloppe pour une chaîne assainie dans n'importe quel langage avec un contrôle de type décent. Mais ce qui suit est (pour autant que je sache) propre aux langages qui supportent les types algébriques.
Les langages qui supportent les types de données algébriques (comme F#) vérifieront également si nous avons manqué certains cas. Par exemple, disons que nous voulons modifier notre fonction d'assainissement pour qu'elle vérifie notre chaîne de caractères et que si elle contient du code malveillant, elle la marque comme telle, sinon elle la marque comme une chaîne sûre. Nous avons maintenant affaire à deux types. De plus, si une chaîne est malveillante, nous ne nous soucions pas de ce qu'elle contient, nous voulons juste indiquer qu'une chaîne malveillante a été reçue. En fait, nous ne voulons pas la garder car nous voulons éviter que quelqu'un l'utilise par erreur. Cela nous donne maintenant ceci :
type SanitizedString =
| Safe of string
| Malicious
En plus de nos autres changements, someLogic
doit gérer cela :
let someLogic (x:SanitizedString) : Results =
match x with
| Safe x -> ...
Mais lorsque j'essaie de construire en F#, j'obtiens ceci :
Incomplete pattern matches on this expression. Par exemple, la valeur "Malicious" peut indiquer un cas non couvert par le(s) modèle(s).
Oui, j'ai oublié d'ajouter la logique pour gérer le cas de Malveillant
! Et comme je l'ai déclaré comme un type de données algébrique, F# l'a vu et m'en a informé. Je n'aurais pas eu cette vérification avec une déclaration ordinaire sur un drapeau. Je corrige donc le code :
let someLogic (x:SanitizedString) : Results =
match x with
| Sanitized x -> ...
| Malicious -> ...
Maintenant, tout va bien dans le monde.
L'utilisation de types a donc également permis de vérifier l'exhaustivité de notre logique. Incidemment, il existe au moins un langage (Idris) qui va plus loin et génère le squelette couvrant tous vos cas pour vous si vous le demandez. Là encore, il utilise des types de données algébriques pour connaître toutes les possibilités.
Prenons maintenant un exemple non lié à la sécurité. Imaginons que nous écrivions un code pour traiter des candidats à l'emploi. Ces candidats ont une variété d'états, avec des données supplémentaires qui varient avec chaque état. Par exemple, un candidat qui...
1. A soumis un curriculum vitae doit inclure le curriculum vitae.
2. A programmé un entretien doit avoir une date et une heure pour l'entretien.
3. Rejeté doit avoir une raison pour le rejet.
4. A reçu une offre d'emploi doit être accompagné d'une offre de salaire.
Ce sont les seuls états légaux ; ainsi, par exemple, un candidat rejeté ne peut pas avoir d'offre de salaire. Cela signifie que nous ne pouvons pas faire cela :
type Resume = ...
type RejectionReason = ...
type StatusType =
| SubmittedResume
| ScheduledInterview
| Rejected
| MadeOffer
type ApplicantStatus = {
firstName: string;
lastName: string;
...
statusType: StatusType;
resume: Resume;
offer: decimal;
rejectionReason: RejectionReason;
interviewDate: DateTime;
}
Cela autoriserait des états illégaux et nous devrions ajouter une logique pour gérer ces états. Au lieu de cela, nous pouvons empêcher la représentation de tels états avec ceci :
type Resume = ...
type RejectionReason = ...
type Applicant = {
firstName: string;
lastName: string;
...
}
type ApplicantStatus =
| SubmittedResume of Applicant * Resume
| ScheduledInterview of Applicant * DateTime
| Rejected of Applicant * RejectionReason
| MadeOffer of Applicant * decimal
Chaque état est accompagné d'un tuple, et un seul type de tuple est autorisé pour chaque état. Comme vous pouvez le voir, ce type applique les états listés ci-dessus sans une seule ligne de logique ; tout est fait à travers le système de types. Nous avons dû réfléchir un peu plus à nos types, mais nous avons récupéré nos efforts en réduisant la logique à écrire et en signalant les erreurs au moment de la construction et pour tous les chemins de code.
En conclusion, il ne suffit pas d'avoir un système de types fort. Nous devons choisir d'utiliser le système de types même lorsque nous n'y sommes pas obligés et nous devons réfléchir soigneusement à nos types. Une astuce pour y parvenir est de remplacer les drapeaux par des types. En s'appuyant plus fortement sur les types, notre système de types devient un meilleur allié, plutôt que l'obstacle que trop de développeurs pensent qu'il est.