Codage sécurisé avec accès simultané dans Swift 4

Dans mon article précédent sur le codage sécurisé dans Swift, j'avais présenté les vulnérabilités de sécurité de base dans Swift, telles que les attaques par injection. Bien que les attaques par injection soient courantes, votre application peut également être compromise. Les conditions de concurrence sont un type de vulnérabilité commun mais parfois négligé. 

Swift 4 présente Accès exclusif à la mémoire, qui consiste en un ensemble de règles pour empêcher l’accès simultané à la même zone de mémoire. Par exemple, le inout L'argument de Swift indique à une méthode qu'il peut changer la valeur du paramètre à l'intérieur de la méthode.

func changeMe (_ x: inout MyObject, etChange y: inout MyObject) 

Mais qu'advient-il si nous passons dans la même variable pour changer en même temps?

changeMe (& myObject, andChange: & myObject) // ???

Swift 4 a apporté des améliorations qui empêchent cette compilation. Mais bien que Swift puisse trouver ces scénarios évidents au moment de la compilation, il est difficile, notamment pour des raisons de performances, de détecter les problèmes d'accès à la mémoire dans un code concurrent, et la plupart des vulnérabilités en matière de sécurité existent sous la forme de conditions de concurrence.

Conditions de course

Dès que plusieurs threads doivent écrire simultanément dans les mêmes données, une situation de concurrence critique peut se produire. Les conditions de course provoquent la corruption des données. Pour ces types d’attaques, les vulnérabilités sont généralement plus subtiles et les exploits plus créatifs. Par exemple, il peut être possible de modifier une ressource partagée pour modifier le flux de code de sécurité se produisant sur un autre thread, ou, dans le cas du statut d'authentification, un attaquant pourrait tirer parti d'un intervalle de temps entre le moment du contrôle. et le temps d'utilisation d'un drapeau.

Le moyen d'éviter les conditions de concurrence est de synchroniser les données. Synchroniser les données signifie généralement les "verrouiller" de sorte qu'un seul thread puisse accéder à cette partie du code à la fois (ce qui est considéré comme un mutex - pour une exclusion mutuelle). Bien que vous puissiez le faire explicitement en utilisant le NSLock classe, il est possible d’oublier des endroits où le code aurait dû être synchronisé. Garder la trace des serrures et s’il est déjà verrouillé ou non peut être difficile.

Grand Central Dispatch

Au lieu d'utiliser des verrous primitifs, vous pouvez utiliser l'API moderne d'accès simultané de Grand Central Dispatch (GCD) -Apple conçue pour la performance et la sécurité. Vous n'avez pas besoin de penser aux serrures vous-même; il fait le travail pour vous dans les coulisses. 

DispatchQueue.global (qos: .background) .async // file d'attente simultanée, partagée par le système // effectue un long travail en arrière-plan ici //…… DispatchQueue.main.async // file d'attente série // Met à jour l'interface utilisateur - show les résultats reviennent sur le fil principal

Comme vous pouvez le constater, c'est une API assez simple, utilisez donc GCD comme premier choix lors de la conception de votre application pour l'accès simultané..

Les contrôles de sécurité à l'exécution de Swift ne peuvent pas être effectués sur les threads GCD, car ils nuisent considérablement aux performances. La solution consiste à utiliser l'outil Thread Sanitizer si vous utilisez plusieurs threads. L'outil Thread Sanitizer est idéal pour rechercher des problèmes que vous ne rencontrerez peut-être jamais en consultant vous-même le code. Il peut être activé en allant à Produit> Schéma> Éditer un schéma> Diagnostics, et en vérifiant le Désinfectant pour fil option.

Si la conception de votre application vous permet de travailler avec plusieurs threads, un autre moyen de vous protéger des problèmes de sécurité liés à la simultanéité consiste à: essayez de concevoir vos cours sans verrou de sorte qu'aucun code de synchronisation n'est nécessaire en premier lieu. Cela nécessite une réelle réflexion sur la conception de votre interface, et peut même être considéré comme un art distinct en soi!

Le vérificateur de fil principal

Il est important de mentionner que la corruption des données peut également se produire si vous effectuez des mises à jour d'interface utilisateur sur un thread autre que le thread principal (tout autre thread est appelé un thread d'arrière-plan).. 

Parfois, il n’est même pas évident que vous soyez sur un fil d’arrière-plan. Par exemple, NSURLSessionde delegateQueue, lorsqu'il est réglé sur néant, sera rappelé par défaut sur un fil d’arrière-plan. Si vous effectuez des mises à jour de l'interface utilisateur ou écrivez dans vos données de ce bloc, les conditions de course sont bonnes. (Résoudre ceci en encapsulant les mises à jour de l'interface utilisateur dans DispatchQueue.main.async ou passer dans OperationQueue.main en tant que file d'attente des délégués.) 

Le vérificateur de thread principal est nouveau dans Xcode 9 et activé par défaut (Produit> Schéma> Modifier le schéma> Diagnostics> Vérification des API d'exécution> Contrôleur de thread principal). Si votre code n'est pas synchronisé, des problèmes apparaîtront dans le Problèmes d'exécution dans le volet de gauche du navigateur de Xcode, soyez donc attentif lors du test de votre application. 

Pour coder pour la sécurité, tous les callbacks ou les gestionnaires d’achèvement que vous écrivez doivent être documentés, qu’ils reviennent ou non sur le thread principal. Mieux encore, suivez la nouvelle conception des API d’Apple qui vous permet de passer Complétion Queue dans la méthode afin que vous puissiez clairement décider et voir quel fil le bloc d'achèvement revient sur.

Un exemple du monde réel

Assez parlé! Plongeons dans un exemple.

class Transaction //… class Transactions private var lastTransaction: Transaction? func addTransaction (_ source: Transaction) //… lastTransaction = source // Premier thread transactions.addTransaction (transaction) // Deuxième thread thread transactions.addTransaction (transaction)

Ici, nous n’avons aucune synchronisation, mais plusieurs threads accèdent aux données en même temps. La bonne chose à propos de Thread Sanitizer est qu’il détectera un cas comme celui-ci. La méthode GCD moderne pour y remédier consiste à associer vos données à une file d'attente de distribution en série..

classe Transactions private var lastTransaction: Transaction? private var queue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") func addTransaction (_ source: Transaction) queue.async // ... self.lastTransaction = source

Maintenant, le code est synchronisé avec le .async bloc. Vous pourriez vous demander quand choisir .async et quand utiliser .synchroniser. Vous pouvez utiliser .async lorsque votre application n'a pas besoin d'attendre la fin de l'opération à l'intérieur du bloc. Il serait peut-être mieux expliqué par un exemple.

let queue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") var transactionIDs: [String] = ["00001", "00002"] // Premier thread file.async transactionIDs.append ("00003") // ne fournissant aucune sortie, il n'est donc pas nécessaire d'attendre la fin de l'opération // Un autre thread file.sync si transactionIDs.contains ("00001") //… Vous devez attendre ici! print ("transaction déjà terminée")

Dans cet exemple, le thread qui demande au tableau de transactions s'il contient une transaction spécifique fournit une sortie. Il doit donc attendre. L'autre thread n'effectue aucune action après l'ajout au tableau de transactions, il n'a donc pas besoin d'attendre la fin du blocage.

Ces blocs sync et async peuvent être encapsulés dans des méthodes qui renvoient vos données internes, telles que des méthodes getter.

get return queue.sync transactionID

Scattering GCD bloque partout dans votre code que l'accès aux données partagées n'est pas une bonne pratique car il est plus difficile de garder une trace de tous les endroits devant être synchronisés. Il est préférable d'essayer de conserver toutes ces fonctionnalités au même endroit.. 

Une bonne conception utilisant des méthodes d'accès est un moyen de résoudre ce problème. L'utilisation des méthodes getter et setter et l'utilisation exclusive de ces méthodes pour accéder aux données signifie que vous pouvez effectuer une synchronisation à un seul endroit. Cela évite d'avoir à mettre à jour de nombreuses parties de votre code si vous modifiez ou refactorez la zone GCD de votre code..

Structs

Tandis que des propriétés stockées uniques peuvent être synchronisées dans une classe, la modification des propriétés d'une structure affectera en réalité la structure entière. Swift 4 inclut désormais une protection pour les méthodes qui modifient les structures. 

Voyons d'abord à quoi ressemble une corruption de structure (appelée "course d'accès rapide").

struct Transaction private var id: UInt32 private var timestamp: Double //… fonction de mutation mutuelle begin () id = arc4random_uniform (101) // 0 - 100 //… fonction de mutation mutc finish () //… horodatage = NSDate ( ) .timeIntervalSince1970

Les deux méthodes de l'exemple changent les propriétés stockées, elles sont donc marquées en mutation. Disons que le thread 1 appelle commencer() et fil 2 appels terminer(). Même si commencer() seulement des changements identifiant et terminer() seulement des changements horodatage, c'est toujours une course d'accès. Bien qu'il soit généralement préférable de verrouiller les méthodes d'accès, cela ne s'applique pas aux structures car l'ensemble de la structure doit être exclusif.. 

Une solution consiste à modifier la structure en classe lorsque vous implémentez votre code simultané. Si vous aviez besoin de la structure pour une raison quelconque, vous pouvez, dans cet exemple, créer un Banque classe qui stocke Transaction structs. Ensuite, les appelants des structures de la classe peuvent être synchronisés.. 

Voici un exemple:

classe Bank private var currentTransaction: Transaction? file d'attente var privée: DispatchQueue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") func doTransaction () queue.sync currentTransaction? .begin () //…

Contrôle d'accès

Il serait inutile de bénéficier de toute cette protection lorsque votre interface expose un objet en mutation ou une UnsafeMutablePointer aux données partagées, car maintenant tout utilisateur de votre classe peut faire ce qu'il veut avec les données sans la protection de GCD. Au lieu de cela, renvoyer des copies aux données dans le getter. Une conception soigneuse de l'interface et de l'encapsulation des données est importante, en particulier lors de la conception de programmes simultanés, pour s'assurer que les données partagées sont réellement protégées..

Assurez-vous que les variables synchronisées sont marquées privé, par opposition à ouvrir ou Publique, qui permettrait aux membres de n’importe quel fichier source d’y accéder. Un changement intéressant dans Swift 4 est que le privé la portée du niveau d'accès est étendue pour être disponible dans les extensions. Auparavant, il ne pouvait être utilisé que dans la déclaration jointe, mais dans Swift 4, un privé la variable est accessible dans une extension, à condition que l'extension de cette déclaration se trouve dans le même fichier source.

Non seulement les variables à risque de corruption des données, mais également les fichiers. Utilisez le Gestionnaire de fichiers Classe Foundation, qui est thread-safe, et vérifiez les indicateurs de résultat de ses opérations sur les fichiers avant de continuer dans votre code.

Interfaçage avec Objective-C

De nombreux objets Objective-C ont une contrepartie modifiable décrite par leur titre.. NSStringLa version mutable de est nommée NSMutableString, NSArrayest de NSMutableArray, etc. Outre le fait que ces objets peuvent être mutés en dehors de la synchronisation, les types de pointeurs provenant d'Objective-C subvertissent également les options Swift. Il y a de bonnes chances que vous attendiez un objet dans Swift, mais dans Objective-C, il est renvoyé avec la valeur nil.. 

Si l'application se bloque, elle donne un aperçu précieux de la logique interne. Dans ce cas, il se peut que les entrées de l'utilisateur n'aient pas été correctement vérifiées et que cette partie du flux de l'application mérite d'être examinée pour essayer d'exploiter.

La solution ici consiste à mettre à jour votre code Objective-C afin d’inclure des annotations nulles. Nous pouvons faire un léger écart ici car ce conseil s’applique à l’interopérabilité en toute sécurité, que ce soit entre Swift et Objective-C ou entre deux autres langages de programmation.. 

Préface vos variables Objective-C avec nullable quand zéro peut être retourné, et non nul quand il ne devrait pas.

- (non-NSString *) myStringFromString: (nullable NSString *) string;

Vous pouvez aussi ajouter nullable et non nul à la liste d'attributs des propriétés Objective-C.

@property (nullable, atomic, strong) NSDate * date;

L'outil Static Analyzer de Xcode a toujours été très utile pour rechercher des bogues Objective-C. Maintenant, avec les annotations de nullabilité, dans Xcode 9, vous pouvez utiliser l'Analyseur statique sur votre code Objective-C et il trouvera des incohérences de nullabilité dans votre fichier. Pour ce faire, accédez à Produit> Effectuer une action> Analyser.

Bien qu’il soit activé par défaut, vous pouvez également contrôler les vérifications de nullabilité dans LLVM avec -Wnullability * drapeaux.

Les contrôles de nullabilité sont utiles pour rechercher des problèmes au moment de la compilation, mais ils ne détectent pas de problèmes d'exécution. Par exemple, nous supposons parfois dans une partie de notre code qu’une valeur optionnelle existera toujours et nous utiliserons le désenroulement forcé. ! dessus. Ceci est une option implicitement non emballée, mais rien ne garantit qu’il existera toujours. Après tout, s’il était marqué facultatif, il serait probablement nul à un moment donné. Par conséquent, il est bon d’éviter de déplier la force avec !. Au lieu de cela, une solution élégante consiste à vérifier au moment de l’exécution de la manière suivante:

gardien laisse dog = animal.dog () else // gérer ce retour de cas // continuer… 

Pour vous aider davantage, une nouvelle fonctionnalité a été ajoutée dans Xcode 9 pour effectuer des contrôles de nullabilité au moment de l'exécution. Il fait partie de l'assainisseur de comportement indéfini et, s'il n'est pas activé par défaut, vous pouvez l'activer en allant à Paramètres de construction> Assainisseur de comportement non défini et mise Oui pour Activer les vérifications d'annotation de nullité.

Lisibilité

C'est une bonne pratique d'écrire vos méthodes avec une seule entrée et un seul point de sortie. Ceci est non seulement bon pour la lisibilité, mais également pour le support avancé du multithreading. 

Supposons qu'une classe a été conçue sans concurrence pour autant. Par la suite, les exigences ont changé de sorte qu’il doit maintenant prendre en charge .fermer à clé() et .ouvrir() Méthodes de NSLock. Lorsque vient le temps de placer des verrous autour de certaines parties de votre code, vous devrez peut-être réécrire un grand nombre de vos méthodes simplement pour être thread-safe. Il est facile de rater un revenir caché au milieu d'une méthode qui devait ensuite verrouiller votre NSLock exemple, ce qui peut alors provoquer une situation de concurrence critique. En outre, des déclarations telles que revenir ne déverrouillera pas automatiquement le verrou. Une autre partie de votre code qui suppose que le verrou est déverrouillé et tente de le verrouiller à nouveau bloquera l'application (l'application sera gelée et sera éventuellement terminée par le système). Les pannes peuvent également être des vulnérabilités de sécurité dans le code multithread si les fichiers de travail temporaires ne sont jamais nettoyés avant la fin du thread. Si votre code a cette structure:

si x si y retourne vrai sinon retourne faux… retourne faux

Vous pouvez plutôt stocker le booléen, le mettre à jour en cours de route, puis le renvoyer à la fin de la méthode. Ensuite, le code de synchronisation peut facilement être intégré à la méthode sans trop de travail.

var success = false // <--- lock if x if y success = true… // < --- unlock return success

le .ouvrir() méthode doit être appelée à partir du même fil que celui appelé .fermer à clé(),  sinon, il en résulte un comportement indéfini.

Essai

Souvent, rechercher et résoudre des vulnérabilités dans du code concurrent revient à rechercher des bogues. Lorsque vous trouvez un bug, c'est comme si vous teniez un miroir par vous-même: une excellente opportunité d'apprentissage. Si vous avez oublié de synchroniser à un endroit, il est probable que la même erreur se trouve ailleurs dans le code. Prendre le temps de vérifier le reste de votre code pour la même erreur lorsque vous rencontrez un bogue est un moyen très efficace de prévenir les vulnérabilités de sécurité qui continueraient à apparaître dans les futures versions de l'application.. 

En fait, bon nombre des jailbreaks récents sur iOS sont dus à des erreurs de codage répétées dans IOKit d’Apple. Une fois que vous connaissez le style du développeur, vous pouvez vérifier les autres parties du code pour rechercher des bogues similaires..

La recherche de bogues est une bonne motivation pour la réutilisation du code. Savoir que vous avez résolu un problème à un endroit et que vous n'avez pas à chercher toutes les mêmes occurrences dans le code copier / coller peut être un grand soulagement..

Les conditions de concurrence peuvent être compliquées à trouver pendant les tests car il est possible que la mémoire doive être corrompue de la bonne manière afin de voir le problème, et parfois, les problèmes apparaissent longtemps après l'exécution de l'application.. 

Lorsque vous testez, couvrez tout votre code. Parcourez chaque flux et chaque cas et testez chaque ligne de code au moins une fois. Parfois, il est utile de saisir des données aléatoires (fuzzing les entrées) ou de choisir des valeurs extrêmes dans l’espoir de trouver un cas extrême qui n’aurait pas été évident en consultant le code ou en utilisant l’application de manière normale. Ceci, associé aux nouveaux outils Xcode disponibles, peut contribuer dans une large mesure à la prévention des vulnérabilités de sécurité. Bien qu'aucun code ne soit sécurisé à 100%, le respect d'une routine, telle que des tests fonctionnels précoces, des tests unitaires, des tests système, des tests de contrainte et des tests de régression, sera réellement rentable.

Au-delà du débogage de votre application, un élément différent de la configuration de la version (la configuration des applications publiées sur le magasin) est que les optimisations de code sont incluses. Par exemple, le compilateur pense qu'une opération non utilisée peut être optimisée ou qu'une variable peut ne pas rester plus longtemps que nécessaire dans un bloc simultané. Pour votre application publiée, votre code est réellement modifié ou différent de celui que vous avez testé. Cela signifie que vous pouvez introduire des bogues qui n'existent que lorsque vous publiez votre application.. 

Si vous n'utilisez pas de configuration de test, assurez-vous de tester votre application en mode de validation en accédant à Produit> Schéma> Edit Scheme. Sélectionner Courir dans la liste de gauche et dans le Info volet de droite, change Configuration de construction à Libération. Bien qu'il soit bon de couvrir l'ensemble de votre application dans ce mode, sachez qu'en raison des optimisations, les points d'arrêt et le débogueur ne se comporteront pas comme prévu. Par exemple, les descriptions de variable peuvent ne pas être disponibles même si le code s'exécute correctement.

Conclusion

Dans cet article, nous avons examiné les conditions de compétition et les moyens de les éviter en codant de manière sécurisée et en utilisant des outils tels que le désinfectant pour fil. Nous avons également parlé de l'accès exclusif à la mémoire, qui constitue un excellent ajout à Swift 4. Assurez-vous qu'il est réglé sur Application intégrale dans Paramètres de construction> Accès exclusif à la mémoire

N'oubliez pas que ces applications ne sont activées que pour le mode débogage et que, si vous utilisez toujours Swift 3.2, de nombreuses applications décrites ne prennent que la forme d'avertissements. Prenez donc les avertissements au sérieux, ou mieux encore, utilisez toutes les nouvelles fonctionnalités disponibles en adoptant Swift 4 dès aujourd'hui.!

Et pendant que vous êtes ici, consultez certains de mes autres articles sur le codage sécurisé pour iOS et Swift.!