iOS-XML auf dem Cloud-Mac prüfen: Entitäten, Namensräume und Ressourcenlimits

iOS-XML auf dem Cloud-Mac prüfen: Entitäten, Namensräume und Ressourcenlimits

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

Ihr nächstes Archiv – erstellt auf einem exklusiven Cloud-Mac

Wählen Sie den Mac passend zu Ihrem Build-Volumen und den Standort passend zu Ihrem Team. Prüfen Sie zuerst die Optionen und übertragen Sie dann Ihre Toolchain.