iOS-XML auf dem Cloud-Mac prüfen: Entitäten, Namensräume und Ressourcenlimits
Ein XML-Import, der lokal problemlos funktioniert, kann eine unbeaufsichtigte Pipeline zum Stillstand bringen: etwa durch Deklarationen externer Entitäten, zu tief verschachtelte Elemente oder gleichnamige Felder aus unterschiedlichen Namensräumen. Die Aufgabe des Cloud-Macs besteht nicht darin, solche Eingaben „so gut wie möglich zu reparieren“. Er soll Dokumente außerhalb der festgelegten Grenzen zuverlässig zurückweisen und reproduzierbare Fehlerbeispiele hinterlassen.
Erst den Eingabevertrag festlegen, dann den Parser aufrufen
Nehmen wir den Import von Einstellungen in einer iOS-App als Beispiel. Zunächst werden die Annahmebedingungen festgelegt: ausschließlich UTF-8, keine DTD, höchstens 256 KiB pro Eingabe, maximal 16 Verschachtelungsebenen und insgesamt höchstens 32 KiB über alle Text-Callbacks. Diese Limits sind Beispiele für diesen Beitrag, keine Vorgaben des XML-Standards oder Standardwerte des Systems. Sie müssen an die fachlichen Anforderungen angepasst werden.
Syntaxfehler, überschrittene Limits und Fehler in fachlichen Feldern werden getrennt protokolliert. Ein erfolgreicher Parser-Durchlauf ist kein Grund, Dokumente mit unbekanntem Wurzelelement, mehrfach vorkommenden einwertigen Feldern oder fehlenden Pflichtangaben anzunehmen.
Die Auflösung externer Entitäten abzuschalten, verbietet weder DTDs noch gewährleistet es eine vollständige Ressourcenisolation. Formatvorgaben, Parser-Einstellungen und Zeitlimits auf Prozessebene müssen jeweils separat geprüft werden.
Eine minimale Sicherheitsprüfung auf dem Cloud-Mac ausführen
Den folgenden Code als xml-guard.swift speichern. Er prüft nur Syntax und Ressourcengrenzen und erzeugt kein fachliches Modell. Das Skript verfolgt einen konservativen Ansatz: Es weist auch das Startzeichen einer DTD-Deklaration innerhalb eines Kommentars zurück. Eine einfache Textprüfung darf nicht als vollständige lexikalische XML-Analyse ausgegeben werden.
import Foundation
final class GuardDelegate: NSObject, XMLParserDelegate {
var depth = 0
var textBytes = 0
var exceeded = false
func stop(_ parser: XMLParser) {
exceeded = true
parser.abortParsing()
}
func parser(
_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String] = [:]
) {
depth += 1
if depth > 16 { stop(parser) }
}
func parser(
_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?
) {
depth -= 1
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
textBytes += string.utf8.count
if textBytes > 32 * 1024 { stop(parser) }
}
func parser(_ parser: XMLParser, foundCDATA data: Data) {
textBytes += data.count
if textBytes > 32 * 1024 { stop(parser) }
}
func parser(
_ parser: XMLParser,
resolveExternalEntityName name: String,
systemID: String?
) -> Data? {
nil
}
}
func validate(_ path: String) throws -> Bool {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { try? handle.close() }
var data = Data()
while true {
guard let chunk = try handle.read(upToCount: 8192),
!chunk.isEmpty else { break }
data.append(chunk)
if data.count > 256 * 1024 { return false }
}
guard let source = String(data: data, encoding: .utf8),
!source.contains("\0"),
!source.contains("<!DOCTYPE") else {
return false
}
let delegate = GuardDelegate()
let parser = XMLParser(data: data)
parser.delegate = delegate
parser.shouldProcessNamespaces = true
parser.shouldResolveExternalEntities = false
parser.externalEntityResolvingPolicy = .never
let parsed = parser.parse()
return parsed && !delegate.exceeded && delegate.depth == 0
}
guard CommandLine.arguments.count == 2 else { exit(2) }
do {
exit(try validate(CommandLine.arguments[1]) ? 0 : 1)
} catch {
exit(2)
}
In einem Terminal mit bereits ausgewählter Toolchain kompilieren und anschließend ein gültiges Beispiel ausführen:
xcrun swiftc xml-guard.swift -o xml-guard
printf '%s\n' '<settings><name>demo</name></settings>' > valid.xml
./xml-guard valid.xml
Für die Exit-Codes gilt: 0 bedeutet bestanden, 1 bedeutet Eingabe zurückgewiesen und 2 bedeutet Fehler beim Aufruf oder beim Lesen der Datei. Die Pipeline sollte anhand dieser Codes unterscheiden, statt sprachabhängige Fehlermeldungen des Systems auszuwerten. Als Eingabe dürfen nur reguläre Dateien dienen, die der Job selbst zwischengespeichert hat; beliebige Gerätepfade dürfen nicht als Eingabe zugelassen werden.
Ablehnungspfade mit festen Testdateien abdecken
Mindestens sechs Dateikategorien sollten erhalten bleiben: gültiges Dokument, nicht geschlossenes Tag, Dokument mit DTD, 17 Verschachtelungsebenen, überschrittenes Textlimit und überschrittenes Eingabebyte-Limit. Die folgenden Befehle erzeugen zwei Testdateien, die jeweils eine Grenze überschreiten. Dafür muss Python 3 in der Laufzeitumgebung verfügbar sein.
python3 - <<'PY'
from pathlib import Path
Path("deep.xml").write_text(
"<n>" * 17 + "</n>" * 17, encoding="utf-8"
)
Path("text-limit.xml").write_text(
"<n>" + "x" * (32 * 1024 + 1) + "</n>", encoding="utf-8"
)
PY
./xml-guard deep.xml
./xml-guard text-limit.xml
Beide Aufrufe müssen 1 zurückgeben. Zusätzlich sollte ein Testpaar genau am Limit und genau ein Byte darüber liegen, um einen falsch gewählten Vergleichsoperator aufzudecken. Ein Beispiel mit externer Entität muss bereits vor dem Parsen zurückgewiesen werden. Wer die Einstellungen des Parsers für externe Ressourcen separat prüft, sollte dafür ein kontrolliertes Testziel verwenden und per Assertion sicherstellen, dass tatsächlich kein Zugriff stattgefunden hat. Ein leeres Ergebnis allein ist kein Sicherheitsnachweis.
Namensräume und Text-Callbacks strukturgerecht verarbeiten
Bei aktivierter Namensraumverarbeitung muss ein fachliches Feld über „Namensraum-URI + lokaler Name“ identifiziert werden. Ein Vergleich nur des Präfixes oder nur von elementName reicht nicht aus. Präfixe können sich ändern; ein name unter einer anderen URI kann dagegen ein anderes Konzept bezeichnen. Die Tests müssen beides abdecken: Ein geändertes Präfix bleibt zulässig, eine geänderte URI muss zur Ablehnung führen.
foundCharacters liefert ein Feld nicht garantiert in einem einzigen Aufruf vollständig. Der eigentliche Decoder sollte einen Elementstapel führen, Text bei jedem Callback anhängen und beim Ende des Elements Länge, Format und mehrfach vorkommende Felder prüfen. CDATA wird über einen eigenen Callback geliefert und darf bei der Zählung nicht fehlen. Auch führende und nachgestellte Leerraumzeichen sollten nicht pauschal aus allen Feldern entfernt werden: Für Anzeigenamen und für Textinhalte mit zu erhaltendem Leerraum können unterschiedliche Verträge gelten.
Keine Teilergebnisse übernehmen
Alle Datensätze werden zunächst in einem temporären, nur dem jeweiligen Job zugänglichen Modell gesammelt. Erst wenn Syntax, Limits, Wurzelelement und fachliche Prüfungen vollständig bestanden sind, erfolgt ein einziger Commit. Dazu absichtlich ein fehlerhaftes Tag an das Dokumentende anhängen und prüfen, dass auch die zuvor eingelesenen Datensätze nicht in die Datenbank gelangen.
Die Abnahme in den Build integrieren statt einmal manuell zu testen
Auf dem Cloud-Mac von OSXBuild werden zunächst die Kommandozeilenbeispiele ausgeführt. Anschließend kommen dieselben Dateien in das iOS-Test-Target, das den tatsächlich von der App verwendeten Decoder aufruft. Beide folgen demselben Eingabevertrag. Ein erfolgreiches Desktop-Skript ersetzt jedoch nicht die Abnahme der Implementierung im Simulator.
Für das Regressionsprotokoll genügen Dateiname, erwarteter Exit-Code, tatsächliches Ergebnis und Toolchain-Version. Der vollständige Importinhalt sollte nicht ausgegeben werden. Fehlerhaft laufende Jobs werden durch ein Pipeline-Zeitlimit begrenzt. Die Limits in den Callbacks beschränken nur bereits übergebene Daten; sie setzen keine absolute Obergrenze für den internen Speicherverbrauch des Parsers.
Abschließend jeden Punkt einzeln bestätigen: Gültige Dateien lassen sich importieren, Dateien außerhalb der Grenzen scheitern zuverlässig, Felder werden nicht über Namensraumgrenzen hinweg verwechselt und nach einem Fehler bleiben keine Teilergebnisse zurück. Bei der nächsten Änderung der Parser-Logik beruht die Abnahme dann weiterhin auf reproduzierbaren Eingaben und eindeutigen Assertions – nicht auf der Feststellung „Diesmal ist nichts hängen geblieben“.
Häufig gestellte Fragen
Reicht es, externe Entitäten zu deaktivieren?
Nein. Diese Einstellung verbietet keine internen DTDs. Das beschriebene Eingabeformat erlaubt nur UTF-8-XML ohne DTD und prüft diese Vorgabe konservativ vor dem Parsen.
Liefert foundCharacters immer ein vollständiges Feld?
Nein. Feldtext kann auf mehrere Aufrufe verteilt sein. Er wird bis zum Elementende gesammelt. Gespeichert wird erst, wenn das gesamte Dokument und die fachliche Prüfung erfolgreich sind.
Ein exklusiver Cloud-Mac für Ihre Build-Pipeline
Ein kompletter physischer Mac nur für Sie – keine virtuelle Maschine. Wählen Sie Modell und Laufzeit passend zu Ihrem Build-Volumen und behalten Sie Archive, Signaturen und Caches auf Ihrem eigenen System.
Cloud-Mac konfigurieren