GRAPHQL API-SECURITY WEB-SECURITY SECURITY BACKEND-SECURITY PERFORMANCE SCALABILITY BEST-PRACTICES DEVSECOPS API-DEVELOPMENT VULNERABILITY-PREVENTION DENIAL-OF-SERVICE QUERY-OPTIMIZATION API-GOVERNANCE SECURITY-BY-DESIGN

Mengamankan API GraphQL Anda Lebih Jauh: Beyond OWASP Top 10 dengan Depth, Complexity, dan Persistent Queries

⏱️ 11 menit baca
👨‍💻

Mengamankan API GraphQL Anda Lebih Jauh: Beyond OWASP Top 10 dengan Depth, Complexity, dan Persistent Queries

1. Pendahuluan

GraphQL telah menjadi pilihan populer bagi banyak developer untuk membangun API yang fleksibel dan efisien. Kemampuannya untuk memungkinkan klien meminta persis data yang mereka butuhkan, dengan struktur bersarang yang dalam, adalah kekuatan utamanya. Namun, kekuatan besar datang dengan tanggung jawab besar, terutama dalam hal keamanan.

Artikel sebelumnya, “GraphQL Security: Mengamankan API GraphQL Anda dari Ancaman Umum (OWASP API Top 10 untuk GraphQL)”, telah membahas fondasi keamanan GraphQL berdasarkan OWASP API Top 10. Itu adalah titik awal yang sangat baik. Tapi, apakah itu cukup?

Sayangnya, fleksibilitas GraphQL juga membuka celah untuk jenis serangan unik yang tidak selalu tercakup oleh OWASP API Top 10 secara langsung. Serangan ini seringkali berfokus pada penyalahgunaan sumber daya server, mengakibatkan Denial of Service (DoS) atau performa yang sangat buruk.

Dalam artikel ini, kita akan melangkah lebih jauh. Kita akan menyelami strategi keamanan lanjutan yang dirancang khusus untuk karakteristik GraphQL: Depth Limiting, Complexity Limiting, dan Persistent Queries. Memahami dan mengimplementasikan mekanisme ini adalah kunci untuk membangun API GraphQL yang tidak hanya fungsional tetapi juga tangguh dan aman di bawah berbagai skenario penggunaan.

2. Tantangan Keamanan Unik GraphQL

📌 Fleksibilitas GraphQL, meskipun sangat menguntungkan, adalah pedang bermata dua. Dengan hanya satu endpoint (misalnya /graphql), klien dapat membuat query yang sangat kompleks dan bersarang, bahkan mengambil seluruh grafik data dalam satu permintaan. Ini berbeda dengan REST API yang biasanya membatasi klien pada endpoint yang lebih spesifik dan terisolasi.

Beberapa tantangan unik yang muncul adalah:

  1. Potensi Serangan Denial of Service (DoS): Query yang terlalu dalam atau terlalu kompleks dapat menghabiskan sumber daya server (CPU, memori, koneksi database) secara eksesif, menyebabkan API melambat atau bahkan crash.
  2. Penyalahgunaan Sumber Daya: Tanpa batasan yang jelas, penyerang atau bahkan klien yang tidak sengaja dapat membuat query yang memicu operasi backend yang mahal (misalnya, banyak join database, fetching data dari microservice yang berbeda secara berantai).
  3. Kesulitan Validasi: Mengingat struktur query yang dinamis, memvalidasi setiap kombinasi field dan argumen secara manual sangatlah sulit.

Untuk mengatasi ini, kita memerlukan mekanisme yang dapat menganalisis dan membatasi query sebelum dieksekusi, serta mengontrol apa saja yang boleh diminta oleh klien.

3. Depth Limiting: Mengatur Kedalaman Query

🎯 Konsep pertama adalah Depth Limiting, yaitu membatasi seberapa dalam sebuah query GraphQL dapat bersarang. Bayangkan sebuah pohon data; depth limiting akan memotong cabang-cabang yang terlalu jauh ke dalam.

💡 Kenapa Penting? Mencegah serangan DoS yang memanfaatkan query bersarang secara rekursif atau mengambil data terkait secara berantai. Misalnya, seorang penyerang bisa mencoba mengambil user -> friends -> friends -> friends... hingga kedalaman yang tak terbatas, yang akan memicu banyak sekali permintaan ke database dan menghabiskan memori server.

Contoh Implementasi Konseptual: Sebagian besar library GraphQL server (seperti Apollo Server atau graphql-js itu sendiri) memiliki plugin atau middleware yang memungkinkan Anda menerapkan depth limiting. Anda akan menentukan batas maksimum kedalaman query yang diiziznkan.

// Contoh pseudo-code untuk middleware Depth Limiting
const depthLimit = (maxDepth) => (schema, query) => {
  let currentDepth = 0;

  // Fungsi rekursif untuk menghitung kedalaman
  const calculateDepth = (node) => {
    if (!node || !node.selectionSet) {
      return 0;
    }

    let maxChildDepth = 0;
    for (const selection of node.selectionSet.selections) {
      if (selection.kind === 'Field') {
        maxChildDepth = Math.max(maxChildDepth, calculateDepth(selection) + 1);
      }
    }
    return maxChildDepth;
  };

  const queryDepth = calculateDepth(query.definitions[0]); // Ambil definisi pertama (query/mutation)

  if (queryDepth > maxDepth) {
    throw new Error(`Query melebihi batas kedalaman maksimum ${maxDepth}. Kedalaman saat ini: ${queryDepth}`);
  }
};

// Penggunaan di Apollo Server (contoh)
const { ApolloServer } = require('apollo-server');
const depthLimitPlugin = require('graphql-depth-limit'); // Library pihak ketiga

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimitPlugin(5)], // Batasi kedalaman query maksimal 5
});

⚠️ Best Practice:

4. Complexity Limiting: Mengelola Beban Komputasi

Depth limiting saja tidak cukup. Sebuah query bisa saja dangkal (misalnya, kedalaman 2), tetapi meminta banyak field di setiap level, atau field yang secara inheren mahal untuk di-resolve. Di sinilah Complexity Limiting berperan.

🎯 Konsep: Complexity limiting memberikan “skor” atau “bobot” pada setiap field dalam skema GraphQL Anda. Ketika sebuah query diterima, total skor kompleksitasnya dihitung, dan jika melebihi ambang batas yang ditentukan, query akan ditolak.

💡 Kenapa Penting? Mencegah serangan DoS yang memanfaatkan query yang meminta banyak data secara paralel atau field yang memicu operasi backend yang mahal. Misalnya, users { id name posts { id title comments { id content } } } mungkin memiliki kedalaman 3, tetapi jika ada 1000 user, 100 post per user, dan 10 comment per post, itu bisa menjadi jutaan record yang di-fetch.

Contoh Implementasi Konseptual: Implementasi complexity limiting biasanya melibatkan penambahan bobot ke definisi field di skema Anda atau melalui konfigurasi di middleware.

// Contoh pseudo-code untuk perhitungan kompleksitas
const calculateComplexity = (schema, query, maxComplexity) => {
  let totalComplexity = 0;

  // Asumsikan kita punya fungsi getFieldComplexity(fieldName)
  // yang mengembalikan bobot default atau custom dari field
  const getFieldComplexity = (fieldName) => {
    switch (fieldName) {
      case 'users': return 10; // Mengambil daftar user bisa mahal
      case 'posts': return 5;
      case 'comments': return 2;
      default: return 1; // Bobot default untuk field sederhana
    }
  };

  // Fungsi rekursif untuk menghitung kompleksitas
  const traverseAndCalculate = (node) => {
    if (!node || !node.selectionSet) {
      return;
    }

    for (const selection of node.selectionSet.selections) {
      if (selection.kind === 'Field') {
        totalComplexity += getFieldComplexity(selection.name.value);
        traverseAndCalculate(selection);
      }
    }
  };

  traverseAndCalculate(query.definitions[0]);

  if (totalComplexity > maxComplexity) {
    throw new Error(`Query melebihi batas kompleksitas maksimum ${maxComplexity}. Kompleksitas saat ini: ${totalComplexity}`);
  }
};

// Penggunaan di Apollo Server dengan library seperti 'graphql-query-complexity'
const { ApolloServer } = require('apollo-server');
const { createComplexityRule, simpleEstimator } = require('graphql-query-complexity');

const complexityRule = createComplexityRule({
  maximumComplexity: 1000, // Batas kompleksitas maksimum
  estimators: [
    simpleEstimator({
      defaultComplexity: 1, // Bobot default
      // Anda bisa menambahkan bobot kustom untuk field tertentu di sini
      // misal: { Query: { users: 100 }, User: { posts: 10 } }
    }),
  ],
  onComplete: (complexity) => {
    console.log('Query Complexity:', complexity);
  },
  create='ValidationRule', // Penting untuk memastikan rule ini berjalan sebelum eksekusi
});

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

Dalam contoh ini, simpleEstimator dapat dikustomisasi untuk memberikan bobot berbeda pada field tertentu. Field yang mengembalikan daftar besar atau memerlukan komputasi berat harus memiliki bobot yang lebih tinggi.

⚠️ Best Practice:

5. Persistent Queries: Mengunci Perilaku Query

💡 Persistent Queries (atau kadang disebut “Automatic Persistent Queries” atau “APQ”) adalah strategi yang lebih ketat untuk mengontrol query yang masuk. Alih-alih membiarkan klien mengirimkan string query GraphQL lengkap, klien hanya diizinkan untuk mengirimkan ID atau hash dari query yang sudah terdaftar sebelumnya di server.

🎯 Konsep:

  1. Developer klien menulis query GraphQL.
  2. Query ini kemudian didaftarkan ke server (biasanya saat proses build/deployment) dan diberikan ID unik atau di-hash.
  3. Server menyimpan mapping antara ID/hash dan string query lengkap.
  4. Saat runtime, klien hanya mengirimkan ID/hash query tersebut, bukan string query lengkapnya. Server akan mencari query lengkap berdasarkan ID/hash dan mengeksekusinya.

Kenapa Penting?

// Contoh Alur Persistent Query
// (1) Di sisi development / build time:
// Klien (misal: Apollo Client) membuat hash dari query.
// Query:
// query GetUserAndPosts($userId: ID!) {
//   user(id: $userId) {
//     id
//     name
//     posts {
//       id
//       title
//     }
//   }
// }
// Hash: sha256_hash_dari_query_ini

// (2) Server menyimpan mapping
// { "sha256_hash_dari_query_ini": "query GetUserAndPosts..." }

// (3) Saat runtime, klien mengirimkan:
fetch('/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    queryId: 'sha256_hash_dari_query_ini', // Klien hanya mengirim ID/hash
    variables: { userId: '123' }
  })
});

// (4) Server menerima request, mencari query lengkap berdasarkan queryId,
// lalu mengeksekusi query GetUserAndPosts dengan variables { userId: '123' }.

⚠️ Best Practice:

6. Strategi Gabungan dan Pertimbangan Lain

Untuk pertahanan yang paling kuat, gabungkan ketiga strategi di atas:

  1. Depth Limiting akan memblokir query yang terlalu dalam.
  2. Complexity Limiting akan memblokir query yang secara komputasi terlalu mahal, bahkan jika kedalamannya tidak ekstrem.
  3. Persistent Queries akan memastikan bahwa hanya query yang sudah disetujui yang dapat dieksekusi sama sekali, memberikan lapisan kontrol dan keamanan tambahan yang proaktif.

Selain itu, jangan lupakan pentingnya praktik keamanan umum:

Kesimpulan

GraphQL menawarkan potensi besar untuk membangun API yang efisien dan fleksibel. Namun, fleksibilitas ini juga menghadirkan tantangan keamanan yang unik, terutama terkait penyalahgunaan sumber daya dan potensi serangan DoS. Mengandalkan OWASP API Top 10 saja tidak cukup; kita perlu strategi yang lebih spesifik untuk GraphQL.

Dengan mengimplementasikan Depth Limiting untuk mengontrol kedalaman bersarang, Complexity Limiting untuk mengelola beban komputasi, dan Persistent Queries untuk mengunci perilaku query yang diizinkan, Anda dapat membangun API GraphQL yang jauh lebih tangguh dan aman. Ini adalah investasi penting untuk menjaga stabilitas dan ketersediaan aplikasi Anda di dunia nyata. Mulailah dengan Depth dan Complexity Limiting, lalu pertimbangkan Persistent Queries untuk kontrol dan keamanan yang lebih proaktif.

🔗 Baca Juga