Secure Coding With Concurrency in Swift 4

In mijn vorige artikel over veilige codering in Swift, besprak ik elementaire beveiligingskwetsbaarheden in Swift zoals injectieaanvallen. Hoewel injectieaanvallen veel voorkomen, zijn er andere manieren waarop uw app kan worden aangetast. Een veel voorkomende maar soms over het hoofd gezien soort van kwetsbaarheid is de raceomstandigheden. 

Swift 4 introduceert Exclusieve toegang tot geheugen, die bestaat uit een reeks regels om te voorkomen dat hetzelfde geheugengebied tegelijk wordt gebruikt. Bijvoorbeeld de in uit argument in Swift vertelt een methode dat het de waarde van de parameter binnen de methode kan wijzigen.

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

Maar wat gebeurt er als we dezelfde variabele doorgeven om tegelijkertijd te veranderen??

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

Swift 4 heeft verbeteringen aangebracht die voorkomen dat dit compileert. Maar terwijl Swift deze voor de hand liggende scenario's kan vinden tijdens het compileren, is het moeilijk om, vooral om prestatieredenen, geheugentoegangsproblemen in gelijktijdige code te vinden, en de meeste beveiligingsrisico's bestaan ​​in de vorm van raceomstandigheden.

Race voorwaarden

Zodra u meer dan één thread hebt die tegelijkertijd naar dezelfde gegevens moet schrijven, kan zich een race-situatie voordoen. Raceomstandigheden veroorzaken datacorruptie. Voor dit soort aanvallen zijn de kwetsbaarheden meestal subtieler en zijn de exploits creatiever. Zo is er bijvoorbeeld de mogelijkheid om een ​​gedeelde bron te wijzigen om de stroom beveiligingscode op een andere thread te wijzigen, of in het geval van een authenticatiestatus kan een aanvaller misbruik maken van een tijdsverschil tussen het tijdstip van controle en het tijdstip van gebruik van een vlag.

De manier om racevoorwaarden te vermijden, is door de gegevens te synchroniseren. Het synchroniseren van gegevens betekent meestal dat het wordt "vergrendeld" zodat slechts één thread toegang heeft tot dat deel van de code tegelijkertijd (naar verluidt een mutex-voor wederzijdse uitsluiting). Hoewel u dit expliciet kunt doen met behulp van de NSLock klasse, is er potentieel om plaatsen te missen waar de code gesynchroniseerd had moeten zijn. Het bijhouden van de sloten en of ze al vergrendeld zijn of niet, kan moeilijk zijn.

Grand Central Dispatch

In plaats van primitieve sloten te gebruiken, kunt u Grand Central Dispatch (GCD) gebruiken - de moderne concurrency-API van Apple, die is ontworpen voor prestaties en beveiliging. U hoeft niet zelf aan de sloten te denken; het doet het werk voor jou achter de schermen. 

DispatchQueue.global (qos: .background) .async // gelijktijdige wachtrij, gedeeld door systeem // doe hier langdurig werk op de achtergrond // // DispatchQueue.main.async // serial queue // Update de UI - show de resultaten terug op de rode draad

Zoals u kunt zien, is het een vrij eenvoudige API, dus gebruik GCD als uw eerste keuze bij het ontwerpen van uw app voor concurrency.

De runtime-beveiligingscontroles van Swift kunnen niet worden uitgevoerd op GCD-threads, omdat dit een aanzienlijke prestatierits oplevert. De oplossing is om de Thread Sanitizer-tool te gebruiken als u met meerdere threads werkt. De Thread Sanitizer-tool is geweldig in het vinden van problemen die je misschien nooit zult tegenkomen door de code zelf te bekijken. Het kan worden ingeschakeld door naar Product> Schema> Wijzigingsschema> Diagnostiek, en het controleren van de Thread Sanitizer keuze.

Als het ontwerp van je app ervoor zorgt dat je met meerdere threads werkt, is een andere manier om jezelf te beschermen tegen de beveiligingsproblemen van concurrency probeer je klassen zo te ontwerpen dat ze slotvrij zijn zodat er in de eerste plaats geen synchronisatiecode nodig is. Dit vereist enige aandacht voor het ontwerp van uw interface en kan zelfs als een afzonderlijke kunst op zich worden beschouwd!

The Main Thread Checker

Het is belangrijk om te vermelden dat datacorruptie ook kan optreden als u UI-updates uitvoert op een andere thread dan de hoofdthread (een andere thread wordt een achtergrondthread genoemd). 

Soms is het niet eens duidelijk dat je een achtergrondthread hebt. Bijvoorbeeld, NSURLSession's delegateQueue, indien ingesteld op nul, zal standaard terugbellen op een achtergrondthread. Als u in dat blok UI-updates uitvoert of naar uw gegevens schrijft, is er een goede kans op raceomstandigheden. (Los dit op door de UI-updates in te pakken DispatchQueue.main.async of passeren OperationQueue.main als de deelnemerswachtrij.) 

Nieuw in Xcode 9 en standaard ingeschakeld is de hoofddraadcontrole (Product> Schema> Schema bewerken> Diagnostiek> Runtime API Checking> Main Thread Checker). Als uw code niet is gesynchroniseerd, verschijnen er problemen in de Runtime-problemen in de linkerpaneel-navigator van Xcode, dus let erop tijdens het testen van uw app. 

Om te coderen voor beveiliging, moeten alle terugbelafhandelingen of voltooiingsafhandelingsmachines die u schrijft, worden gedocumenteerd, ongeacht of ze terugkeren naar de hoofddraad of niet. Beter nog, volg Apple's nieuwere API-ontwerp waarmee je een a kunt passeren completionQueue in de methode, zodat u duidelijk kunt beslissen en zien welke thread het voltooiingsblok terugkeert.

Een real-world voorbeeld

Genoeg gepraat! Laten we een voorbeeld nemen.

class Transactie // ... class Transacties private var lastTransaction: Transaction? func addTransaction (_ source: Transaction) // ... lastTransaction = source // Eerste thread transacties.addTransaction (transactie) // Tweede thread transacties.addTransaction (transactie)

Hier hebben we geen synchronisatie, maar meer dan één thread heeft toegang tot de gegevens op hetzelfde moment. Het goede aan Thread Sanitizer is dat het een geval als dit zal detecteren. De moderne GCD-manier om dit te verhelpen is om uw gegevens te koppelen aan een seriële verzendwachtrij.

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

Nu wordt de code gesynchroniseerd met de .async blok. Je vraagt ​​je misschien af ​​wanneer je moet kiezen .async en wanneer te gebruiken .synchroniseren. Je kunt gebruiken .async wanneer uw app niet hoeft te wachten totdat de bewerking in het blok is voltooid. Het kan beter worden uitgelegd met een voorbeeld.

let queue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") var transactionIDs: [String] = ["00001", "00002"] // Eerste thread queue.async transactionIDs.append ("00003") // geen uitvoer leveren, dus niet wachten totdat het is voltooid // Nog een thread queue.sync if transactionIDs.contains ("00001") // ... Moet hier wachten! print ("Transactie al voltooid")

In dit voorbeeld biedt de thread die de transactiereeks vraagt ​​als deze een specifieke transactie bevat, uitvoer, dus deze moet wachten. De andere thread onderneemt geen actie nadat deze is toegevoegd aan de transactiearray, dus hoeft niet te wachten totdat het blok is voltooid.

Deze sync- en async-blokken kunnen worden ingepakt in methoden die uw interne gegevens retourneren, zoals gettermethoden.

krijg return queue.sync transactionID

Verspreiding GCD blokkeert alle delen van uw code die toegang hebben tot gedeelde gegevens is geen goede gewoonte, omdat het moeilijker is om alle plaatsen bij te houden die moeten worden gesynchroniseerd. Het is veel beter om te proberen al deze functionaliteit op één plek te houden. 

Een goed ontwerp met behulp van accessormethoden is een manier om dit probleem op te lossen. Het gebruik van methoden voor getter en setter en alleen het gebruik van deze methoden om toegang tot de gegevens te krijgen, betekent dat u op één plaats kunt synchroniseren. Dit voorkomt dat u veel delen van uw code moet bijwerken als u het GCD-gebied van uw code wijzigt of refactoring.

structs

Terwijl enkele opgeslagen eigenschappen kunnen worden gesynchroniseerd in een klasse, zullen veranderende eigenschappen op een struct feitelijk de gehele structuur beïnvloeden. Swift 4 bevat nu bescherming voor methoden die de structs muteren. 

Laten we eerst eens kijken naar wat een struct-corruptie (een "Swift-toegangswedstrijd" genoemd) eruit ziet.

struct Transactie private var id: UInt32 private var timestamp: Double // ... mutating func begin () id = arc4random_uniform (101) // 0 - 100 // ... muting func finish () // ... timestamp = NSDate ( ) .timeIntervalSince1970

De twee methoden in het voorbeeld veranderen de opgeslagen eigenschappen, dus ze zijn gemarkeerd muteren. Laten we zeggen thread 1-aanroepen beginnen() en rij 2 oproepen af hebben(). Zelfs indien beginnen() alleen veranderingen ID kaart en af hebben() alleen veranderingen tijdstempel, het is nog steeds een toegangswedstrijd. Hoewel het normaal is om binnen accessor-methoden te vergrendelen, is dit niet van toepassing op structs, omdat de gehele struct exclusief moet zijn. 

Een oplossing is om de struct in een klasse te veranderen bij het implementeren van uw concurrentcode. Als u de struct om een ​​of andere reden nodig had, kunt u in dit voorbeeld een maken Bank klasse die opslaat Transactie structs. Vervolgens kunnen de aanroepers van de structs binnen de klasse worden gesynchroniseerd. 

Hier is een voorbeeld:

class Bank private var currentTransaction: Transaction? private var queue: DispatchQueue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") func doTransaction () queue.sync currentTransaction? .begin () // ...

Toegangscontrole

Het zou zinloos zijn om al deze bescherming te hebben wanneer uw interface een muterend object of een UnsafeMutablePointer naar de gedeelde gegevens, omdat nu elke gebruiker van uw klas kan doen wat ze wil met de gegevens zonder de bescherming van GCD. Stuur in plaats daarvan kopieën terug naar de gegevens in de getter. Zorgvuldig interfaceontwerp en gegevensinkapseling zijn belangrijk, vooral bij het ontwerpen van gelijktijdige programma's, om te zorgen dat de gedeelde gegevens echt worden beschermd.

Zorg ervoor dat de gesynchroniseerde variabelen zijn gemarkeerd privaat, in tegenstelling tot Open of openbaar, waarmee leden van elk bronbestand toegang kunnen krijgen. Een interessante verandering in Swift 4 is dat de privaat bereik op accessniveau is uitgebreid om beschikbaar te zijn in extensies. Voorheen kon het alleen worden gebruikt binnen de insluitende verklaring, maar in Snel 4, a privaat variabele kan worden geopend in een extensie, zolang de extensie van die verklaring zich in hetzelfde bronbestand bevindt.

Niet alleen zijn er variabelen die een risico lopen op gegevensbeschadiging, maar ook bestanden. Gebruik de Bestandsbeheer Foundation-klasse, die thread-safe is, en controleer de resultaatvlaggen van de bestandsbewerkingen voordat u doorgaat in uw code.

Omkadering met Objective-C

Veel Objective-C-objecten hebben een veranderlijke tegenhanger die wordt weergegeven door hun titel. NSStringde veranderbare versie is genoemd NSMutableString, NSArray'zus NSMutableArray, enzovoorts. Naast het feit dat deze objecten kunnen worden gemuteerd buiten de synchronisatie, onderdrukken aanwijzertypen die afkomstig zijn van Objective-C ook Swift-opties. Er is een goede kans dat je een object in Swift kunt verwachten, maar vanuit Objective-C wordt het als nul teruggegeven. 

Als de app crasht, geeft dit waardevol inzicht in de interne logica. In dit geval kan het zijn dat de gebruikersinvoer niet correct is gecontroleerd en dat gedeelte van de app-stroom de moeite van het bekijken waard is om te proberen en te exploiteren.

De oplossing hier is om uw Objective-C-code bij te werken zodat annotaties voor nullability worden opgenomen. We kunnen hier een beetje afwijken, aangezien dit advies van toepassing is op veilige interoperabiliteit in het algemeen, of dit nu tussen Swift en Objective-C is of tussen twee andere programmeertalen. 

Voorwoord uw Objective-C variabelen met nullable wanneer nihil kan worden geretourneerd, en nonnull wanneer het niet zou moeten.

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

Je kunt ook toevoegen nullable en nonnull naar de attributenlijst van Objective-C eigenschappen.

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

De Static Analyzer-tool in Xcode was altijd al geweldig voor het vinden van Objective-C-bugs. Nu met annotaties voor nullabiliteit, kunt u in Xcode 9 de Static Analyzer gebruiken voor uw Objective-C-code en vindt u inconsistenties voor niet-compatibiliteit in uw bestand. Doe dit door te navigeren naar Product> Actie uitvoeren> Analyseren.

Hoewel het standaard is ingeschakeld, kunt u ook de controle op nullability in LLVM beheren met -Wnullability * vlaggen.

Niet-controleerbaarheidscontroles zijn goed voor het vinden van problemen tijdens het compileren, maar ze vinden geen runtime-problemen. Soms nemen we bijvoorbeeld in een deel van onze code aan dat een optionele waarde altijd zal bestaan ​​en de force unwrap gebruiken ! ben ermee bezig. Dit is een impliciet niet-ingepakte optie, maar er is geen garantie dat deze altijd zal bestaan. Immers, als het als optioneel was gemarkeerd, is het op een gegeven moment waarschijnlijk nihil. Daarom is het een goed idee om het uitpakken met geweld te voorkomen !. In plaats daarvan is een elegante oplossing om runtime als volgt te controleren:

bewaker let dog = animal.dog () else // behandel deze case return // ga verder ... 

Om u verder te helpen, is er een nieuwe functie toegevoegd in Xcode 9 om tijdens runtime controles op nullability uit te voeren. Het maakt deel uit van het Undefined Behavior Sanitizer en hoewel het niet standaard is ingeschakeld, kunt u het inschakelen door naar Bouw instellingen> Undefined Behavior Sanitizer en instellen Ja voor Nullability Annotation Checks inschakelen.

Leesbaarheid

Het is een goede gewoonte om uw methoden met slechts één begin- en eindpunt te schrijven. Dit is niet alleen goed voor de leesbaarheid, maar ook voor geavanceerde multithreading-ondersteuning. 

Laten we zeggen dat een klasse zonder concurrency in gedachten is ontworpen. Later veranderden de vereisten, zodat deze nu het .slot() en .unlock () methodes van NSLock. Wanneer het tijd is om sloten rond delen van uw code te plaatsen, moet u mogelijk veel van uw methoden herschrijven om zo thread-safe te zijn. Het is gemakkelijk om een ​​te missen terugkeer verborgen in het midden van een methode die je later moest vergrendelen NSLock Dit kan bijvoorbeeld een race-toestand veroorzaken. Ook verklaringen zoals terugkeer zal het slot niet automatisch ontgrendelen. Een ander deel van uw code dat ervan uitgaat dat het slot ontgrendeld is en opnieuw probeert te vergrendelen, blokkeert de app (de app zal bevriezen en uiteindelijk door het systeem worden beëindigd). Crashes kunnen ook beveiligingsrisico's zijn in multithread-code als tijdelijke werkbestanden nooit worden opgeschoond voordat de thread wordt beëindigd. Als uw code deze structuur heeft:

if x if y return true anders return false ... return false

U kunt in plaats daarvan de Boolean opslaan, deze onderweg bijwerken en aan het einde van de methode retourneren. Dan kan de synchronisatiecode zonder veel moeite in de methode worden ingepakt.

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

De .unlock () methode moet worden aangeroepen vanuit dezelfde thread die heeft gebeld .slot(),  anders resulteert dit in ongedefinieerd gedrag.

testen

Het vinden en oplossen van kwetsbaarheden in gelijktijdige code komt vaak neer op het zoeken naar bugs. Als je een fout ontdekt, is het alsof je jezelf een spiegel voorhoudt - een geweldige kans om te leren. Als u bent vergeten om op één plaats te synchroniseren, is het waarschijnlijk dat dezelfde fout elders in de code voorkomt. De tijd nemen om de rest van uw code te controleren op dezelfde fout wanneer u een fout tegenkomt, is een zeer efficiënte manier om beveiligingskwetsbaarheden te voorkomen die steeds weer verschijnen in toekomstige app-releases. 

In feite zijn veel van de recente iOS-jailbreaks veroorzaakt door herhaalde coderingsfouten die zijn aangetroffen in de IOKit van Apple. Zodra u de stijl van de ontwikkelaar kent, kunt u andere delen van de code controleren op soortgelijke bugs.

Het vinden van fouten is een goede motivatie voor hergebruik van code. Wetende dat je een probleem op één plek hebt opgelost en niet dezelfde plaats hoeft te vinden in de kopieer- / plakcode, kan een grote opluchting zijn.

Raceomstandigheden kunnen moeilijk te vinden zijn tijdens het testen, omdat het geheugen op de 'juiste manier' moet worden beschadigd om het probleem te kunnen zien, en soms verschijnen de problemen lang in de uitvoering van de app. 

Voer tijdens het testen al uw code uit. Doorloop elke flow en case en test elke regel code minstens eenmaal. Soms helpt het om willekeurige gegevens in te voeren (fuzzing de ingangen), of extreme waarden te kiezen in de hoop een randgeval te vinden dat niet vanzelfsprekend zou zijn als je naar de code kijkt of de app op een normale manier gebruikt. Dit, samen met de nieuwe beschikbare Xcode-tools, kan een lange weg afleggen naar het voorkomen van beveiligingskwetsbaarheden. Hoewel geen enkele code 100% veilig is, zal het volgen van een routine, zoals functionele tests, eenheidstests, systeemtest, stress- en regressietests, echt lonend zijn.

Naast het debuggen van uw app, is een ding dat anders is voor de releaseconfiguratie (de configuratie voor apps die in de winkel zijn gepubliceerd) dat code-optimalisaties zijn inbegrepen. Wat de compiler bijvoorbeeld denkt, is dat een ongebruikte bewerking kan worden geoptimaliseerd, of dat een variabele niet langer dan nodig blijft in een gelijktijdig blok. Voor uw gepubliceerde app is uw code feitelijk gewijzigd of anders dan de code die u heeft getest. Dit betekent dat er bugs kunnen worden geïntroduceerd die alleen bestaan ​​nadat u uw app heeft uitgebracht. 

Als u geen testconfiguratie gebruikt, moet u uw app testen in de releasemodus door naar te navigeren Product> Schema> Wijzig regeling. kiezen Rennen uit de lijst aan de linkerkant en in de info deelvenster rechts, wijzigen Configuratie bouwen naar Vrijlating. Hoewel het goed is om uw volledige app in deze modus te gebruiken, weet u dat vanwege optimalisaties de breekpunten en de foutopsporingsfunctie niet werken zoals verwacht. Variabelenbeschrijvingen zijn bijvoorbeeld mogelijk niet beschikbaar, ook al wordt de code correct uitgevoerd.

Conclusie

In dit bericht hebben we gekeken naar raceomstandigheden en hoe je ze kunt vermijden door veilig te coderen en tools zoals de Thread Sanitizer te gebruiken. We hebben ook gesproken over Exclusive Access to Memory, wat een geweldige toevoeging is voor Swift 4. Zorg ervoor dat het is ingesteld op Volledige handhaving in Bouw instellingen> Exclusieve toegang tot geheugen

Vergeet niet dat deze handhavingsmaatregelen alleen gelden voor de debug-modus en als u nog steeds Swift 3.2 gebruikt, komen veel van de besproken handhavingsmaatregelen alleen in de vorm van waarschuwingen. Dus neem de waarschuwingen serieus, of beter nog, maak gebruik van alle nieuwe functies die beschikbaar zijn door vandaag Swift 4 te adopteren!

En terwijl je hier bent, bekijk enkele van mijn andere berichten over veilige codering voor iOS en Swift!