Strikte Base64URL-Dekodierung für iOS auf dem Cloud-Mac testen
Eine API kodiert einen binären Cursor als Base64URL, doch der iOS-Client akzeptiert gelegentlich auch beschädigte Cursor als gültige Daten. Die Ursache muss nicht im Netzwerk liegen: Eine tolerante Dekodierung, automatisch ergänztes Padding und das Ignorieren ungültiger Zeichen können Eingaben in Bytes umwandeln, die eigentlich zurückgewiesen werden müssten. Auf einem Cloud-Mac lässt sich der Dekodierungsvertrag zunächst mit einem eigenständigen Swift-Skript festschreiben. Anschließend können dieselben Testfälle in das Test-Target der Anwendung übernommen werden. So lassen sich die Grenzen der Kodierung prüfen, ohne den Simulator zu starten.
Zuerst die zulässige Kodierung festlegen
Dieser Artikel akzeptiert ausschließlich nicht leeres Base64URL ohne Padding: Buchstaben, Ziffern sowie - und _. Leerzeichen, Zeilenumbrüche, +, / und = sind nicht erlaubt. Das ist eine ausdrückliche Festlegung für die API der Anwendung, keine allgemeine Vorgabe für alle Protokolle, die Base64 verwenden.
Standard-Base64 und Base64URL sollten nicht über denselben Einstiegspunkt durch automatisches Erraten des Formats unterstützt werden. Benötigt eine ältere API Padding, sollte sie einen eigenen Einstiegspunkt für die Dekodierung erhalten und separat getestet werden. So erweitert ein Client-Update nicht unbemerkt die Menge der akzeptierten Eingaben.
Auch für die Länge gelten strukturelle Vorgaben: Bei Text ohne Padding darf die Anzahl der Zeichen bei Division durch vier nur den Rest null, zwei oder drei ergeben. Ein Rest von eins kann keine gültige Kodierung bilden. Das Beispiel begrenzt die Eingabe auf 65536 UTF-8-Bytes. Das ist eine Anwendungsregel für diesen Test, kein vom Protokoll vorgegebener Wert.
Die Dekodierung stellt lediglich Bytes wieder her. Sie belegt nicht, dass deren Inhalt vertrauenswürdig ist. Bei Token, Cursorn oder Signaturparametern müssen weiterhin Struktur, Herkunft und fachliche Gültigkeit geprüft werden.
Die kanonische Darstellung durch erneutes Kodieren erzwingen
Speichern Sie den folgenden Code als base64url-check.swift. Zunächst wird die Eingabegröße begrenzt, dann das Alphabet geprüft und erst danach intern Padding ergänzt. Verwenden Sie keine Option, die unbekannte Zeichen ignoriert, und entfernen Sie vorab keine Leerraumzeichen.
import Foundation
enum DecodeFailure: Error {
case size
case alphabet
case length
case payload
case noncanonical
}
func decodeBase64URL(
_ text: String,
maxEncodedBytes: Int = 65536
) throws -> Data {
let count = text.utf8.count
guard count > 0, count <= maxEncodedBytes else {
throw DecodeFailure.size
}
let valid = text.utf8.allSatisfy { byte in
(65...90).contains(byte)
|| (97...122).contains(byte)
|| (48...57).contains(byte)
|| byte == 45
|| byte == 95
}
guard valid else {
throw DecodeFailure.alphabet
}
let remainder = count % 4
guard remainder != 1 else {
throw DecodeFailure.length
}
var padded = text
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
padded += String(repeating: "=", count: (4 - remainder) % 4)
guard let data = Data(base64Encoded: padded) else {
throw DecodeFailure.payload
}
let canonical = data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
guard canonical == text else {
throw DecodeFailure.noncanonical
}
return data
}
Der abschließende Vergleich darf nicht entfallen. Die kanonische Darstellung des einzelnen Bytes FF lautet _w. Bei _x sind die ungenutzten Restbits nicht null; manche Decoder liefern trotzdem dasselbe Byte zurück. Durch das erneute Kodieren und Vergleichen hängt die Annahme einer Eingabe nicht davon ab, wie tolerant der zugrunde liegende Decoder arbeitet.
Die Funktion gibt hier Data zurück und wandelt das Ergebnis nicht direkt in einen String um. Gültige Binärdaten sind nicht zwangsläufig gültiges UTF-8. Benötigt die aufrufende Schicht Text, muss sie die Zeichenkodierung gesondert prüfen, statt beschädigte Inhalte durch Ersatzzeichen zu verdecken.
Ungültige Testfälle als ausführbare Prüfungen formulieren
Fügen Sie die folgenden Tests am Ende derselben Datei ein. Schlägt ein Test fehl, sorgt ein geworfener Fehler dafür, dass das Skript mit einem von null verschiedenen Exit-Code endet. Die Prüfung hängt damit nicht von Assertions ab, die in optimierten Builds entfernt werden können.
enum CheckFailure: Error {
case mismatch
case acceptedInvalid
}
let validCases: [(String, [UInt8])] = [
("AA", [0]),
("_w", [255]),
("-_8", [251, 255]),
("Zm9v", [102, 111, 111])
]
for (text, expected) in validCases {
guard try decodeBase64URL(text) == Data(expected) else {
throw CheckFailure.mismatch
}
}
let invalidCases = [
"", "A", "Zg==", "Zg\n", "+w", "/w", "_x", "中文"
]
for text in invalidCases {
do {
_ = try decodeBase64URL(text)
} catch is DecodeFailure {
continue
}
throw CheckFailure.acceptedInvalid
}
do {
_ = try decodeBase64URL("AAAA", maxEncodedBytes: 3)
} catch is DecodeFailure {
exit(0)
}
throw CheckFailure.acceptedInvalid
Führen Sie auf einem Cloud-Mac mit eingerichteten Kommandozeilenwerkzeugen folgenden Befehl aus:
xcrun swift base64url-check.swift
Bei Erfolg gibt es keine Ausgabe, und der Exit-Code ist null. _x deckt die Prüfung der Restbits ab. Der Testfall mit Zeilenumbruch verhindert, dass später unbemerkt eine automatische Bereinigung eingebaut wird. Der Testfall mit chinesischen Zeichen bestätigt, dass das Alphabet anhand von ASCII-Bytes geprüft wird und nicht danach, ob ein Zeichen „wie ein Buchstabe aussieht“.
Größenlimits und die Platzierung der Prüfungen kontrollieren
Die Größenprüfung muss vor den Zeichenersetzungen und der Dekodierung erfolgen. Andernfalls hat eine überlange Eingabe bereits zusätzliche Kopien verursacht. Die größte hier zulässige kanonische Eingabe kann zu 49152 Bytes dekodiert werden. Das ist jedoch nur das Budget auf Kodierungsebene; für fachliche Felder sollten meist kleinere Obergrenzen gelten.
Der an die Funktion übergebene String befindet sich bereits im Speicher. Die Funktion ersetzt deshalb keine Größenbegrenzung für Netzwerkantworten. Die Empfangsschicht muss den Antwortkörper begrenzen, die Dekodierungsschicht das einzelne Feld und die Schicht für die Strukturanalyse die Anzahl der Elemente in Sammlungen sowie die Verschachtelungstiefe. Jede der drei Schichten trägt ihre eigene Verantwortung.
Ergänzen Sie außerdem drei Grenzfälle:
- Gültiger Text, dessen Länge genau der Obergrenze entspricht, muss akzeptiert werden.
- Eine Überschreitung um ein einziges Zeichen muss zum Fehler führen. Die Eingabe darf nicht vor dem Dekodieren gekürzt werden.
- Text mit zulässiger Länge, aber ungültigem Inhalt muss weiterhin zurückgewiesen werden.
Verwenden Sie das Dekodierungsergebnis nicht als Identifikator, während andere Verarbeitungspfade zugleich tolerant dekodierte Varianten akzeptieren. Welche Darstellung für Cache-Schlüssel, Deduplizierungsschlüssel und Signaturdaten gilt, muss im API-Vertrag einheitlich festgelegt sein. Andernfalls kann derselbe Inhalt unterschiedlich behandelt werden.
Regressionstests einbinden und Diagnosedaten begrenzen
Sobald das eigenständige Skript erfolgreich durchläuft, verschieben Sie die Dekodierungsfunktion in ein gemeinsam genutztes Modul und übernehmen Sie die Testfälle in das Unit-Test-Target der Anwendung. Das Kommandozeilenskript eignet sich zur schnellen Reproduktion. Tests im Anwendungs-Target decken dagegen Unterschiede bei den tatsächlichen Abhängigkeiten und der Build-Konfiguration auf. Zwei getrennte Implementierungen sollten nicht dauerhaft parallel gepflegt werden.
Zusätzlich zu den festen Testfällen können Sie Byte-Arrays unterschiedlicher Länge kodieren, wieder dekodieren und anschließend Byte für Byte vergleichen. Decken Sie vor allem die unterschiedlichen Reste bei der Division der Länge durch drei ab. Sie bestimmen, wie viele Restbits die letzte Kodierungsgruppe verwendet. Verändern Sie danach das letzte Zeichen, um sicherzustellen, dass nicht kanonische Darstellungen nicht unbemerkt akzeptiert werden.
Fehlerberichte sollten nur die Fehlerkategorie, die Eingabelänge und die Testfallnummer enthalten, niemals echte Token oder vollständige Cursor. Prüfen Sie vor der Bereitstellung: keine automatische Entfernung von Leerraumzeichen, kein Ignorieren unbekannter Zeichen, keine Vermischung von Alphabeten und eine Strukturvalidierung erst nach der Dekodierung. Das Ziel ist nicht, möglichst viele Eingaben erfolgreich zu dekodieren, sondern ausschließlich die zwischen beiden Seiten vereinbarte Darstellung zu akzeptieren.
Häufig gestellte Fragen
Warum die dekodierten Bytes erneut kodieren?
Manche Dekoder akzeptieren ungenutzte Restbits mit einem Wert ungleich null. Erneutes Kodieren in die kanonische Form ohne Padding und der Vergleich mit der Eingabe erkennen solche Varianten.
Prüft der Dekoder auch die Signatur eines Tokens?
Nein. Er prüft nur die Kodierungsform. Signatur, Ablaufzeit und Aussteller müssen durch eine separate kryptografische Prüfung und Anwendungsregeln validiert werden.
Cloud-Mac mit eigener Pipeline testen
ArmRDP bietet dedizierte physische Mac mini statt virtueller Maschinen. Mieten Sie tageweise und prüfen Sie zuerst Toolchain und Build-Ergebnisse, bevor Sie die passende Laufzeit für Ihr Team wählen.
Cloud-Mac mieten