DevMastery Logo

🔥 Newsletter o programowaniu

Jak ograniczyć głębokość zapytań w GraphQL?

Michał Taszycki
Michał Taszycki

100 ludzi x 10 przyjaciół, co może się stać?...

Budujemy serwis GraphQL. Załóżmy że mamy takie prościutkie schema:

type Query {
  users: [User!]!
}
type User {
  id: ID!
  name: String
  friends: [User!]!
}

Typ użytkownika zawiera listę przyjaciół, kryjącą się pod kluczem friends. Możemy zatem spytać o listę użytkowników, ich identyfikatory, imiona, a także o ich przyjaciół.

Załóżmy też, że baza danych nie jest duża.

Mamy tylko

  • 100 użytkowników, a każdy z nich ma
  • 10 przyjaciół.

Korzystamy z GraphQLa więc bardzo łatwo możemy zapytać o wszystkich użytkowników z ich przyjaciółmi.

query GetUsersWithFriends {
  users {
    name
    friends {
      name
    }
  }
}

Po wykonaniu zapytania dostajemy JSON zawierający 100 użytkowników po 10 przyjaciół każdy. Łącznie 1000 rekordów reprezentujących przyjaciół.

Serwer bez problemu uciągnie takie zapytanie więc nie ma co się martwić...

...prawda?

With great power comes great responsibility...

GraphQL daje nam dużą dowolność w konstruowaniu zapytań. Dzięki temu możemy łatwo poznać zależności w grafie przyjaciół.

Jeśli na przykład chcemy poznać kogo znają przyjaciele naszych użytkowników wystarczy wysłać takie zapytanie:

query GetFriendsOfFriends {
  users {
    name
    friends {
      name
      friends {
        name
      }
    }
  }
}

Dostaniemy 100 x 10 x 10 = 10 000 rekordów. Serwer nadal sobie z tym poradzi bez problemu.

Ale, niestety nic nie stoi na przeszkodzie aby ktoś skonstruował takie zapytanie.

query DenialOfService {
  users {
    name
    friends {
      name
      friends {
        name
        friends {
          name
          friends {
            name
            friends {
              name
            }
          }
        }
      }
    }
  }
}

W takiem przypadku serwer musi zwrócić 100 x 10 x 10 x 10 x 10 x 10 czyli 10 milionów rekordów.

Jeśli to nie spowoduje problemu na serwerze to osoba atakująca może bardzo łatwo dodać jeszcze 50 takich zagnieżdżeń.

I zabić nasz serwer jednym zapytaniem.

Wypróbuj samodzielnie jak poziom zagnieżdżeń wpływa na czas odpowiedzi.

Na szczęście zapobieganie tego typu atakom jest równie proste jak ich wykonaywanie.

Z pomocą przychodzi nam:

Ograniczanie głębokości zapytań

Gdy mamy cykliczne relacje pomiędzy typami w GraphQLu, pojawia się ryzyko rekurencyjnych zapytań. Zawsze w takiej sytuacji powinniśmy ograniczyć maksymalną głębokość zagnieżdżeń w zapytaniu.

Wtedy nasz serwer, już na etapie walidacji zapytania może zdecydować czy takie zapytanie przepuścić czy odrzucić.

Możemy taką walidację napisać samodzielnie, ale najprościej będzie skorzystać z biblioteki.

W JavaScripcie mamy na przykład bibliotekę graphql-depth-limit. Dzięki niej możemy, za pomocą zaledwie kilku linijek kodu zabezpieczyć serwer przed podobnymi atakami.

Najpierw budujemy regułę walidacji dla określonej głębokości.

const MAX_DEPTH = 2;
const createDepthLimit = require("graphql-depth-limit");
const depthLimit = createDepthLimit(MAX_DEPTH);

A potem dodajemy tę regułę do konfiguracji serwera.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit],
});

Teraz gdy ktoś wykona zbytnio zagnieżdżone zapytanie, serwer natychmiastowo zwróci błąd.

Błąd walidacji nadmiernie zagnieżdżonego zapytania

Ale rozsądne zapytania przechodzą bez problemu.

Możesz je przetestować samodzielnie, poniżej.

OK! A co jeśli?...

No dobra, a jeśli zamiast 100 użytkowników z 10 przyjaciół, będziemy mieć ich 10000 po 1000 przyjaciół każdy?

Nawet jedno zagnieżdżenie zmusi serwer do zwrócenia 10 milionów rekordów!

Cóż, tym problemem zajmiemy się następnym razem...

Nie przegap kolejnych artykułów 📚

Zostaw mi swój email a ja napiszę Ci gdy nowy artykuł pojawi się na blogu. Od czasu do czasu wyślę Ci również ciekawe materiały dotyczące programowania.

Zapisując się na newsletter będziesz ode mnie otrzymywać okazjonalne maile związane z programowaniem. Możesz się z niego wypisać w dowolnym momencie (jeden klik).