Sichere Dekodierung von iOS-Archiven auf dem Cloud-Mac testen
Wenn eine neue App-Version Archive einer älteren Version einliest, ist ein Fehler nicht das gefährlichste Ergebnis. Kritischer ist, wenn der Lesevorgang leere Daten zurückgibt und der anschließende Speichervorgang damit die ursprüngliche Datei überschreibt. Prüfen Sie diesen Ablauf auf einem Cloud-Mac: zuerst mit einem reinen Foundation-Dekodierungstest, anschließend mit denselben Beispieldaten in einem iOS-Testtarget. Dabei geht es nicht darum, einen einzigen erfolgreichen Schreib-Lese-Durchlauf nachzuweisen. Entscheidend ist, dass falsche Typen, unbekannte Versionen und beschädigte Eingaben ausdrücklich abgewiesen werden.
Zuerst die Grenzen des Lesevorgangs festlegen
Dieser Beitrag behandelt Objektarchive, die mit NSKeyedArchiver erzeugt wurden, nicht die Verschlüsselung. Das Beispiel erlaubt genau ein Wurzelobjekt mit einer Versionsnummer und einem Textfeld. Vor der Abnahme werden drei verbindliche Bedingungen festgelegt:
- Das Wurzelobjekt muss ein
Memosein. Es darf nicht zunächst ein beliebiges Objekt angenommen und anschließend per erzwungenem Cast umgewandelt werden. - Die Version muss dem aktuell unterstützten Wert entsprechen. Der Text muss vorhanden sein und eine festgelegte Maximallänge einhalten.
- Bei einem Fehler darf weder die Originaldatei überschrieben noch ein leerer Datensatz als erfolgreiches Ergebnis ausgegeben werden.
supportsSecureCoding ist eine Deklaration des Typs und prüft fachliche Feldbedingungen nicht automatisch. Die lesende Seite muss weiterhin requiresSecureCoding aktivieren und jedes Objektfeld mit einer Methode dekodieren, die die zulässigen Typen einschränkt.
Sichere Dekodierung begrenzt, welche Objekttypen instanziiert werden dürfen. Sie bietet weder eine Herkunftsauthentifizierung noch Vertraulichkeit oder ein Dateigrößenlimit. Begrenzen Sie die Eingabegröße, bevor Sie die Daten an den Decoder übergeben.
Einen Decoder schreiben, der ungültige Eingaben abweist
Speichern Sie den folgenden Inhalt als archive-check.swift. Er benötigt ausschließlich Foundation. Prüfen Sie zunächst, ob sich xcrun swift --version ausführen lässt. Die Versionsnummer im Beispiel bezeichnet die Version des Archivprotokolls, nicht die Betriebssystemversion.
import Foundation
final class Memo: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
let schema: Int
let text: String
init(schema: Int = 1, text: String) {
self.schema = schema
self.text = text
super.init()
}
required init?(coder: NSCoder) {
guard coder.containsValue(forKey: "schema"),
coder.decodeInteger(forKey: "schema") == 1,
let value = coder.decodeObject(
of: NSString.self, forKey: "text"
),
value.length <= 4096 else {
return nil
}
schema = 1
text = value as String
super.init()
}
func encode(with coder: NSCoder) {
coder.encode(schema, forKey: "schema")
coder.encode(text as NSString, forKey: "text")
}
}
enum ArchiveFailure: Error {
case invalidSize
case invalidObject
}
func readMemo(_ data: Data) throws -> Memo {
guard !data.isEmpty, data.count <= 1_048_576 else {
throw ArchiveFailure.invalidSize
}
let reader = try NSKeyedUnarchiver(forReadingFrom: data)
reader.requiresSecureCoding = true
reader.decodingFailurePolicy = .setErrorAndReturn
let memo = reader.decodeObject(
of: Memo.self, forKey: NSKeyedArchiveRootObjectKey
)
reader.finishDecoding()
if let error = reader.error {
throw error
}
guard let memo else {
throw ArchiveFailure.invalidObject
}
return memo
}
func pack(_ value: Any) throws -> Data {
try NSKeyedArchiver.archivedData(
withRootObject: value, requiringSecureCoding: true
)
}
Zunächst wird geprüft, ob das Versionsfeld vorhanden ist. So kann der Standardwert der Integer-Dekodierung einen fehlenden Schlüssel nicht verdecken. Die Textlänge wird wie bei NSString.length in UTF-16-Codeeinheiten gemessen. Wählen Sie die Obergrenze für die produktive Implementierung anhand Ihres Produktprotokolls. Der Beispielwert ist kein allgemeingültiger Sicherheitsstandard.
setErrorAndReturn muss vor dem Einlesen des Objekts gesetzt werden. Verlassen Sie sich nicht darauf, dass ein äußeres do/catch sämtliche Dekodierungsfehler abfängt: Bei manchen ungültigen Objekten greift die Fehlerstrategie des Decoders statt des üblichen Swift-Pfads für geworfene Fehler.
Mit vier Eingaben eine minimale Regressionssuite aufbauen
Ergänzen Sie die Datei um den folgenden Code. Beim gültigen Beispiel muss der ursprüngliche Text wieder eingelesen werden; die drei anderen Beispiele müssen scheitern. Sobald ein Ergebnis von der Erwartung abweicht, beendet sich das Prüfprogramm mit einem Exitstatus ungleich null.
func expectRejection(_ data: Data) {
do {
_ = try readMemo(data)
} catch {
return
}
fatalError("Unexpected archive acceptance")
}
let valid = try pack(Memo(text: "draft"))
let restored = try readMemo(valid)
guard restored.text == "draft" else {
fatalError("Round-trip mismatch")
}
expectRejection(try pack("wrong root" as NSString))
expectRejection(try pack(Memo(schema: 2, text: "future")))
expectRejection(Data(valid.prefix(12)))
Kompilieren Sie das Programm vor der Ausführung. Damit vermeiden Sie, dass eine interpretierte Ausführungsumgebung die Typnamen der nachfolgenden Beispieldaten beeinflusst:
set -eu
xcrun swiftc archive-check.swift -o archive-check
./archive-check
Bei ungültigen Beispieldaten kann Foundation Diagnosemeldungen auf der Standardfehlerausgabe ausgeben. Das bedeutet nicht, dass das Prüfprogramm fehlgeschlagen ist. Maßgeblich sind der Exitstatus des Prozesses und die Assertions, nicht ein vollständig leeres Protokoll. Wird ein falsches Wurzelobjekt akzeptiert, muss zuerst die Positivliste korrigiert werden. Entfernen Sie keine Negativfälle, nur damit die Tests wieder grün werden.
Auf verschachtelte Objekte und historische Beispieldateien erweitern
Zulässige Typen pro Feld einschränken
Enthält ein reales Archiv Arrays, sind deren Elemente nicht schon deshalb sicher, weil NSArray zugelassen wurde. Verwenden Sie die Überladung von decodeObject(of:forKey:), die eine Menge von Klassen akzeptiert. Nehmen Sie sowohl den Container als auch die erwarteten Elementklassen in diese Menge auf und prüfen Sie anschließend Anzahl, Feldlängen und Beziehungen. Fügen Sie nicht die weit gefasste Klasse NSObject hinzu, um einen Fehler zu umgehen.
Typbeschränkungen für Archive ersetzen keine Kontrolle über die Komplexität des Objektgraphen. Auch ein Eingabegrößenlimit garantiert keinen begrenzten Speicherverbrauch nach der Dekodierung. Für nicht vertrauenswürdige Eingaben sind zusätzlich ein eingeschränktes Protokoll oder eine separate Verarbeitungsgrenze erforderlich.
Tatsächliche Dateien älterer Versionen als Testressourcen festhalten
Nehmen Sie Archive älterer Versionen, aus denen sensible Daten entfernt wurden, als Testressourcen ins Repository auf. Dokumentieren Sie die erzeugende Version, die erwarteten Felder und die zulässigen Migrationsergebnisse. Dynamisch erzeugte Beispieldaten eignen sich zur Prüfung der Ablehnungslogik. Sie belegen aber nicht, dass die von einer alten Version tatsächlich geschriebenen Bytes weiterhin lesbar sind.
Der Laufzeitname einer Swift-Klasse kann sich mit dem Modul ändern. Prüfen Sie beim Verschieben von Typen oder beim Umbenennen eines Moduls zuerst die historischen Beispieldateien. Konfigurieren Sie anschließend eine Klassenzuordnung für die ausdrücklich festgelegten alten Namen. Beliebige Klassennamen aus der Eingabe auszulesen und automatisch freizugeben, ist nicht zulässig. Auch nach erfolgreichen Kommandozeilentests müssen dieselben Dateien im iOS-Testtarget geprüft werden, um Unterschiede der Ziellaufzeit abzudecken.
Fehler auf die Leseschicht begrenzen, statt sie bis zum Speichern weiterzutragen
Bilden Sie das Dekodierungsergebnis als Erfolg oder eindeutigen Fehler ab. Vermeiden Sie try? mit einem leeren Ersatzobjekt. Bleibt das Einlesen erfolglos, muss die Originaldatei erhalten bleiben; die Oberfläche sollte einen Wiederherstellungsweg anzeigen. Erst wenn die Migration abgeschlossen ist und die neuen Daten durch erneutes Einlesen validiert wurden, darf die alte Datei ersetzt werden.
Prüfen Sie vor dem Merge fünf Punkte: Die Felder des gültigen Beispiels stimmen überein; ein falscher Wurzeltyp wird abgewiesen; eine unbekannte Version wird abgewiesen; abgeschnittene Eingaben lösen kein Zurückschreiben als vermeintlichen Erfolg aus; historische Beispieldateien bestehen die Tests im iOS-Target. Legen Sie für jeden Negativfall einen eigenen Test an, damit bei einem Fehler sofort erkennbar ist, welche Bedingung verletzt wurde.
Das Kommandozeilenprogramm auf dem Cloud-Mac liefert schnelle Rückmeldung, die iOS-Tests bestätigen das Verhalten auf der Zielplattform. Erst wenn beide Ebenen erfolgreich sind, liegen reproduzierbare Abnahmenachweise für den Lesepfad der Archive vor – und nicht nur ein einmaliger Erfolg auf dem Entwicklungsrechner.
Häufig gestellte Fragen
Reicht supportsSecureCoding aus?
Nein. Aktivieren Sie requiresSecureCoding beim Lesen, beschränken Sie Klassen mit den passenden decodeObject-Methoden und prüfen Sie Version und Feldwerte zusätzlich.
Darf ein Lesefehler eine leere Liste ergeben?
Behandeln Sie den Fehler nicht als erfolgreiches leeres Ergebnis. Geben Sie einen Fehler zurück und behalten Sie die Quelldatei, damit spätere Schreibvorgänge sie nicht unbemerkt ersetzen.
Mieten Sie einen Cloud-Mac für Ihren nächsten Build
Exklusiver physischer Mac mini, keine virtuelle Maschine. Wählen Sie eine Laufzeit von einem Tag, einer Woche, einem Monat oder einem Quartal. Konfiguration und Verfügbarkeit entnehmen Sie bitte der Konsole.