Zum Inhalt springen

GraphQL-Introspektion und ungeschützte Felder

CWE-200CWE-285OWASP A05:2021Aktualisiert 4. September 20265 Min. Lesezeit

Mit Introspektion kann jeder das vollständige GraphQL-Schema abrufen: jeden Typ, jedes Feld und jede Mutation, auch das, was Ihr eigenes Frontend nie nutzt. Da bei GraphQL der Client bestimmt, welche Felder er abfragt, muss die Autorisierung je Feld erfolgen. Geschieht das nicht, ist alles abrufbar, was im Schema steht.

GraphQL verlagert einen Teil der Steuerung zum Client: Dieser bestimmt selbst, welche Felder er abfragt. Darin liegt die Stärke des Modells und zugleich der Grund, weshalb die Absicherung dort anders funktioniert. Im Folgenden lesen Sie, warum eine Prüfung je Endpunkt hier nicht genügt und was Introspektion dabei preisgibt.

Was ist Introspektion?

Introspektion ist eine eingebaute Eigenschaft von GraphQL, mit der ein Client das vollständige Schema abrufen kann: alle Typen, alle Felder samt Datentypen, alle Abfragen und alle Mutationen. Die Funktion besteht, um Werkzeuge zu ermöglichen, einen Editor, der beim Tippen ergänzt, eine Dokumentation, die sich selbst erzeugt, und während der Entwicklung ist sie ausgesprochen nützlich.

In der Produktion ist sie ein vollständiges Inhaltsverzeichnis Ihres Datenmodells, abrufbar für jeden, der den Endpunkt erreichen kann. Hinzu kommt der zweite und wichtigere Punkt: Da der Client bestimmt, welche Felder er abfragt, ist das Schema zugleich eine Liste dessen, was verfügbar ist. Fehlt eine Prüfung je Feld, lässt sich alles, was im Schema steht, auch tatsächlich abrufen.

Der Unterschied zu einer klassischen API liegt genau dort. Bei REST bestimmen Sie je Endpunkt, was zurückgeht; die Zahl möglicher Antworten ist endlich und überschaubar. Bei GraphQL bestimmt der Client die Form seiner Frage, und Ihr Schema ist die Grenze. Was Sie in das Schema schreiben, sollten Sie auch absichern.

Wie missbraucht ein Angreifer die Introspektion?

Verwundbar:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: true,          // auch in der Produktion
});
# Jeder kann das vollständige Schema abrufen
query { __schema { types { name fields { name type { name } } } } }

Aus der Antwort geht hervor, welche Typen es gibt, samt Feldern, die das Frontend nie nutzt. Ein Angreifer stellt anschließend schlicht seine eigene Frage:

query {
  nutzer {
    id
    name
    email
    passwortHash        # steht im Schema, also abrufbar
    mfaGeheimnis
    interneNotiz
  }
}

Das offizielle Frontend fragt nur id und name ab. Dass die übrigen Felder existieren, genügt: Es gibt keine Prüfung, die besagt, dass passwortHash nicht für diesen Nutzer bestimmt ist. Eine einzige Abfrage liefert damit Daten, die über eine klassische API nie einen Endpunkt erhalten hätten.

Sicher:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  validationRules: [depthLimit(7), complexityLimit(1000)],
  formatError: (fehler) => ({ message: 'Anfrage konnte nicht verarbeitet werden' }),
});
// Autorisierung am Feld selbst, nicht an der Abfrage
const resolvers = {
  Nutzer: {
    email: (eltern, args, ctx) => {
      if (ctx.nutzer.id !== eltern.id && ctx.nutzer.rolle !== 'admin') {
        return null;
      }
      return eltern.email;
    },
    // Felder, die nie nach außen dürfen, gehören nicht ins Schema
  },
  Query: {
    nutzer: async (eltern, args, ctx) => {
      if (!ctx.nutzer) throw new GraphQLError('Nicht authentifiziert');
      return nutzerRepo.sichtbarFuer(ctx.nutzer);
    },
  },
};

Hier geschehen vier Dinge. Introspektion ist nur außerhalb der Produktion eingeschaltet. Tiefe und Komplexität von Abfragen sind begrenzt, was eine Überlastung über tief verschachtelte Fragen verhindert. Fehlermeldungen schlagen keine Feldnamen mehr vor. Und das Wichtigste: Die Autorisierung sitzt am Feld, wo sie hingehört. Felder, die unter keinen Umständen nach außen dürfen, Passwort-Hashwerte, Zwei-Faktor-Geheimnisse, stehen gar nicht erst im Schema.

Betrachten Sie Ihr Schema als öffentlich, auch bei abgeschalteter Introspektion. Es lässt sich aus dem JavaScript-Bündel Ihres Frontends ableiten, aus Fehlermeldungen, die ähnliche Feldnamen vorschlagen, und durch systematisches Ausprobieren. Die Absicherung muss in den Resolvern stecken, nicht in der Unbekanntheit des Schemas.

Welche Auswirkungen hat GraphQL-Introspektion?

Der Schweregrad reicht von mittel bis hoch und hängt davon ab, was ohne Feldprüfung erreichbar ist. Bei einem Schema, in dem nur öffentliche Daten stehen, bleibt es bei Informationen über Ihr Datenmodell. Stehen dort sensible Felder ohne eigene Autorisierung, ist es ein Datenleck, das mit einer einzigen Abfrage ausgeführt wird.

Das Risiko erhöht sich durch die Verbindung mit anderen Eigenschaften von GraphQL. Es gibt meist einen einzigen Endpunkt, wodurch Beschränkungen je Endpunkt wenig bedeuten. Eine Anfrage kann Daten aus vielen Typen zugleich abrufen, wodurch ein Angreifer in wenigen Anfragen viel erhält. Und verschachtelte Beziehungen ermöglichen es, über einen Umweg an Daten zu gelangen, die auf der obersten Ebene sehr wohl geschützt sind, etwa indem der Nutzer einer Nachricht abgefragt wird statt der Nutzer unmittelbar.

Hinzu kommt die Belastungsseite. Eine tief verschachtelte Abfrage kann den Server zu einer enormen Menge Arbeit zwingen, was ohne Tiefen- und Komplexitätsgrenzen zu einem Ausfall führen kann.

Wie spürt man GraphQL-Introspektion auf?

Ein Tester probiert zunächst mit einer üblichen Schema-Abfrage, ob Introspektion eingeschaltet ist. Gelingt das, steht das vollständige Schema zur Verfügung, und die Aufmerksamkeit richtet sich sofort auf die Felder, die das Frontend nicht nutzt.

Ist Introspektion abgeschaltet, wird das Schema dennoch rekonstruiert: aus dem JavaScript-Bündel, aus den Abfragen, die die Anwendung selbst versendet, und indem Feldnamen ausprobiert und die Fehlermeldungen beobachtet werden, denn Umsetzungen, die bei einem Tippfehler eine Korrektur vorschlagen, verraten damit, welche Felder sehr wohl bestehen. Anschließend wird je gefundenem Feld mit einem Konto mit minimalen Rechten geprüft, ob eine Autorisierung greift. Ebenso wird auf verschachtelte Pfade geachtet, die um eine Prüfung herumführen, auf die Begrenzung von Tiefe und Komplexität und darauf, ob Mutationen dieselben Prüfungen kennen wie Abfragen. AssistSec prüft GraphQL-Endpunkte gesondert vom Rest der API, denn auf REST ausgerichtete Standardwerkzeuge machen hier wenig sichtbar.

Wie verhindert man GraphQL-Introspektion?

  • Schalten Sie Introspektion in der Produktion ab und lassen Sie sie nur in Entwicklungsumgebungen eingeschaltet.
  • Autorisieren Sie je Feld in den Resolvern, nicht nur auf Abfrage- oder Mutationsebene.
  • Nehmen Sie Felder, die nie nach außen dürfen, gar nicht erst in das Schema auf.
  • Begrenzen Sie Tiefe und Komplexität von Abfragen und beschränken Sie die Zahl der Ergebnisse je Ebene.
  • Geben Sie neutrale Fehlermeldungen aus und schalten Sie Vorschläge für ähnliche Feldnamen ab.
  • Legen Sie eine Ratenbegrenzung fest, die die Kosten einer Abfrage berücksichtigt, nicht nur die Zahl der Anfragen.
  • Prüfen Sie verschachtelte Beziehungen, denn diese können einen Umweg um ein geschütztes Hauptfeld bilden.
  • Wenden Sie auf Mutationen dieselbe Autorisierung an wie auf Abfragen.
  • Betrachten Sie Ihr Schema als öffentliche Information und lassen Sie die Absicherung nicht auf Unbekanntheit ruhen.

Quellen

Häufige Fragen

Genügt das Abschalten der Introspektion?

Nein, es ist allenfalls eine Hürde. Das Schema lässt sich auch aus dem JavaScript-Bündel Ihres Frontends ableiten, aus Fehlermeldungen, die Feldnamen vorschlagen, und durch systematisches Ausprobieren. Die eigentliche Maßnahme ist die Autorisierung je Feld; das Abschalten erschwert lediglich die Erkundung.

Warum ist Autorisierung je Feld nötig?

Weil bei GraphQL der Client selbst zusammenstellt, welche Felder er möchte. Eine Prüfung auf Abfrageebene sagt nichts darüber aus, ob genau dieses Feld für diesen Nutzer bestimmt ist. Steht ein sensibles Feld ohne eigene Prüfung im Schema, ist es abrufbar, auch wenn Ihr Frontend nie danach fragt.

Was ist ein Tiefenangriff?

Eine Abfrage, die über Beziehungen tief verschachtelt aufgebaut wird, etwa Autor zu Beiträgen zu Autor und so fort. Ohne Begrenzung kann eine einzige Anfrage den Server enorm belasten. Legen Sie eine Höchsttiefe und eine Komplexitätsgrenze fest und begrenzen Sie die Zahl der Ergebnisse je Ebene.

Sollte ich Fehlermeldungen anpassen?

Ja. GraphQL-Umsetzungen schlagen bei einem unbekannten Feld häufig einen ähnlichen Namen vor. Das ist während der Entwicklung hilfreich und in der Produktion ein Erkundungswerkzeug. Schalten Sie diese Vorschläge ab und geben Sie neutrale Fehlermeldungen aus.

Verwandte Artikel

Zum Suchen / drücken · Esc