Disable GraphQL Introspection pada Reverse Proxy
Motivasi
GraphQL Introspection pada dasarnya adalah fitur yang dapat memudahkan bagi siapa saja yang ingin berinteraksi dengan GraphQL API, karena dengan fitur inspeksi kita dapat mengetahui resource apa saja yang tersedia pada schema-nya seperti query, type, field dan lain sebagainya.
Contoh, jika kita ingin mengetahui type
apa saja yang tersedia di dalam sebuah GraphQL API, maka kita cukup mengirimkan query seperti berikut:
{
__schema {types {name}}}
Maka, pada GraphQL API yang mengaktifkan fitur Introspection
akan mengirimkan respon berupa type
apa saja yang tersedia pada endpoint tersebut:
{
"data": {
"__schema": {
"types": [
{
"name": "Boolean"
},
{
"name": "Float"
},
{
"name": "ID"
},
{
"name": "Int"
},
{
"name": "String"
},
{
"name": "__Directive"
},
{
"name": "__DirectiveLocation"
},
{
"name": "__EnumValue"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "query_root"
}
]
}
}
}
Untuk beberapa kasus, fitur ini adalah sesuatu yang memudahkan, salah satunya pada endpoint yang bersifat open yang semua orang diperbolehkan untuk menggunakannya. Keuntungannya, tidak harus banyak menulis dokumentasi teknis karena dengan mudah kita bisa mendapatkannya, bahkan dapat mencobanya dengan GraphQL Playground ataupun GraphiQL.
Namun, jika hal tersebut diaktifkan pada API yang sifatnya private dan hanya source dengan kriteria tertentu saja yang dapat menggunakannya, maka perlu menjadi pertimbangan apakah fitur Introspection
tersebut benar-benar harus diaktifkan atau tidak. Karena tentu saja ini dapat menjadi Security Hole yang dapat dimanfaatkan oleh pihak-pihak yang tidak bertanggung jawab.
Jika kamu tertarik untuk eksploit GraphQL API dari celah Introspection
, tulisan berikut mungkin dapat memberi gambaran: How to exploit GraphQL endpoint: introspection, query, mutations & tools.
Penanggulangannya tentu saja dengan menonaktifkan fitur GraphQL Introspection. Pada library atau framework yang memungkinkan kita untuk membangun sebuah GraphQL server, fitur tersebut biasanya by default disediakan. Misalnya sebuah server GraphQL yang dibangun menggunakan Apollo yang running diatas Node JS, kita hanya perlu menonaktifkan introspection
pada bagian konfigurasi:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false // disable introspection
});
Namun, bagaimana jika fitur tersebut tidak tersedia?
Sejak GraphQL populer, banyak tool yang berkaitan dengan hal itu tersedia. Salah satu jenis tool yang juga cukup populer adalah hadirnya PaaS, BaaS, DBaaS maupun SaaS yang menawarkan solusi instant GraphQL untuk kebutuhan development, baik yang sifat nya open-source maupun yang closed-source. Sehingga kita dapat lebih fokus pada pengembangan produk tanpa memikirkan kompleksitas teknis khususnya pada layer data. Layanan-layanan tersebut misalnya Fauna, Hasura, Supabase, Strapi, Nhost dan banyak lagi yang lainnya. Beberapa diantaranya ada yang menyediakan fitur tersebut by default, ada juga yang ketersediannya melalui sebuah mekanisme upgrade layanan. Contohnya pada Hasura, fitur tersebut hanya tersedia untuk pengguna cloud dan enterprise.
Jika kasusnya seperti diatas, maka kita dapat melakukan workaround dengan cara intercept request pada level API Gateway maupun Reverse Proxy. Caranya adalah dengan memvalidasi setiap request pada metode POST
, lalu cek apakah pada request body terdapat keyword __schema
atau __type
yang mengindikasikan bahwa request tersebut adalah introspeksi
. Jika ditemukan, maka lakukan drop request dengan memberikan pesan error, namun jika bukan maka teruskan request.
Implementasi
Kita akan coba lakukan Proof of Concept ini pada Nginx yang bertindak sebagai Reverse Proxy. Untuk intercept lalu validate request body pada setiap request, kita akan menggunakan js module
sebagai tool nya.
Pertama, pasang js module
atau njs
(Nginx JavaScript) pada nginx:
$ sudo apt-get install nginx-module-njs
Edit file konfigurasi Nginx:
nano /etc/nginx/nginx.conf
Load module njs pada file konfigurasi
load_module modules/ngx_http_js_module.so;
Lalu wrap konfigurasi dengan scope http
http {....
server {
....
}}
Buat file JavaScript pada /etc/nginx/njs/introspect.js
Buat sebuah fungsi dengan parameter r
yang akan kita pakai untuk mengolah request (r = request).
function disableSchema(r) {
// code her
}
export default {disableSchema}
Pertama, kita cek apakah request method nya post
atau bukan
if(r.method === 'POST'){
}
Jika sesuai, cek apakah pada body request terdapat keyword __schema
atau type
jika iya, maka drop request dengan mengirimkan error sesuai dengan spec error GraphQL
var requestBody = JSON.stringify(r.requestBody)
if(requestBody.includes("__schema") || requestBody.includes("__type")){
r.headersOut['Content-Type'] = 'application/json';
r.return(
200,
JSON.stringify(
{
"errors": [
{
"message": "GraphQL introspection is not allowed."
}
]
}
)
)
}
Jika request nya bukan bersifat introspeksi
, maka teruskan request ke GraphQL server
r.internalRedirect('@app-backend')
Maka file lengkap introspect.js
adalah sebagai berikut:
function disableSchema(r) {
if(r.method === 'POST'){
var requestBody = JSON.stringify(r.requestBody)
if(requestBody.includes("__schema") || requestBody.includes("__type")){
r.headersOut['Content-Type'] = 'application/json';
r.return(
200,
JSON.stringify(
{
"errors": [
{
"message": "GraphQL introspection is not allowed."
}
]
}
)
)
} else r.internalRedirect('@app-backend')
} else {
r.internalRedirect('@app-backend')
}
}
export default {disableSchema}
Kembali lagi ke file konfigurasi, pada scope http
, import introspect.js
yang telah dibuat tadi
js_import /etc/nginx/njs/introspect.js;
Lalu uat upstream
yang mengarah ke server backend dimana GraphQL server di serve
upstream backend {
server localhost:8080;
}
Selanjutnya, pada scope server
, buat dua buah location
: /
sebagai root dan @app-backend
sebagai variable yang diakses dari file js tadi.
server {
server_name your-url.com;location / {
js_content introspect.disableSchema;
}location @app-backend {
proxy_pass http://backend;
}
}
Pada root diarahkan ke fungsi pada introspect.js
untuk divalidasi apakah request berupa introspeksi atau bukan. Lalu pada @app-backend
diarahkan ke server GraphQL.
Jangan lupa untuk memakai SSL dan redirect http-nya.
server {
server_name your-url.com;location / {
js_content introspect.disableSchema;
}location @app-backend {
proxy_pass http://backend;
}listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/your-url.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-url.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}server {
if ($host = your-url.com) {
return 301 https://$host$request_uri;
}listen 80;
server_name your-url.com;
return 404; # managed by Certbot
}
File lengkap /etc/nginx/nginx.conf
adalah sebagai berikut:
load_module modules/ngx_http_js_module.so;http {js_import /etc/nginx/njs/introspect.js;upstream backend {
# graphql server
server localhost:8080;
}server {
server_name your-url.com;location / {
js_content introspect.disableSchema;
}location @app-backend {
proxy_pass http://backend;
}listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/your-url.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-url.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}server {
if ($host = your-url.com) {
return 301 https://$host$request_uri;
}listen 80;
server_name your-url.com;
return 404; # managed by Certbot
}
}
Cek apakah konfigurasi-nya tepat atau tidak:
~# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
Restart nginx
~# systemctl restart nginx
Terakhir, lakukan request introspeksi, maka hasilnya akan seperti berikut:
{
"errors": [
{
"message": "GraphQL introspection is not allowed."
}
]
}
Selamat, GraphQL Introspection sudah dinonaktifkan via reverse proxy. Implementasinya dapat berbeda pada setiap reverse proxy. Yang harus kamu concern adalah bagaimana intercept, validate atau bahkan modifikasi setiap request pada reverse proxy. Jika ketemu, maka sesuaikan workaround nya dengan kerangka yang sama seperti pada implementasi.