Shopify Admin dibangun di atas GraphQL, dan di situlah sebagian besar bug bernilai ditemukan. Shopify sendiri, lewat panduan “Hacking GraphQL at Shopify”, mengakui peningkatan laporan di area Admin API yang nggak terdokumentasi. Artinya permukaan GraphQL memang tempat bug hidup. Artikel ini membahas secara teknis cara testing IDOR dan permission di GraphQL Shopify — berdasarkan pola dari report yang udah disclosed, lengkap dengan query dan script yang bisa kamu copy-paste.

Catatan legal: semua testing harus dilakukan di dev store milik kamu sendiri (email @wearehackerone.com). Menyentuh store orang lain = langsung banned. Kalau belum setup, baca Setup Shopify Dev Store dulu.

Kenapa GraphQL jadi ladang bug

GraphQL memindahkan logika query ke client. Satu endpoint menerima banyak operation, masing-masing menerima argumen termasuk object ID. Kesalahan klasik di backend: validasi ownership object nggak konsisten antar operation. Developer sering mengamankan operation “utama” tapi lupa operation turunan yang mengakses node yang sama.

Dua kelas bug utama di sini:

Kelas bugDeskripsiContoh dibayar
IDOR / BOLA antar-shopObject ID dari shop A bisa diakses dari konteks shop B#2207248 ($5.000)
Permission boundary violationStaff tanpa permission bisa jalankan operation yang butuh Sensitive Permission#417170 (staff payments)

Langkah 1 — Dump semua GraphQL operations

Buka Network tab browser saat kamu pakai Shopify Admin secara normal (buka halaman produk, order, billing, settings). Filter request ke endpoint GraphQL. Untuk tiap operation, catat:

  • Nama operation.
  • Argumen yang diterima — khususnya yang berupa ID.
  • Jenis ID: numerik/incremental vs UUID/GID.
  • Data sensitif yang dikembalikan (PII, finansial, payment).

Operation yang menerima ID numerik dan mengembalikan data sensitif adalah prioritas tertinggi. Contoh struktur request GraphQL yang kamu lihat di Network tab:

POST /admin/internal/web/graphql/core HTTP/2
Host: admin.shopify.com
Content-Type: application/json
Cookie: _secure_session_id=...

{
  "operationName": "BillingDocumentDownload",
  "variables": { "id": "gid://shopify/BillingInvoice/1001" },
  "query": "query BillingDocumentDownload($id: ID!) { ... }"
}

Perhatikan bagian "id" — kalau nilainya numerik dan incremental, itu kandidat IDOR.

Langkah 2 — Uji IDOR antar-shop

Ini pola yang terbukti dibayar. Contoh nyata: operation download billing document (BillingDocumentDownload / BillDetails) menerima invoice ID yang bisa di-increment. Dengan swap ID, hunter bisa mengakses PII plus last-4 digit kartu kredit merchant lain — dibayar $5.000 (report #2207248).

Prosedur uji (semua di dua dev store milik kamu sendiri):

  1. Di dev store A, jalankan operation dan catat ID object kamu (misal invoice ID = 1001).
  2. Coba ID lain (1000, 1002, dst) atau ID dari store B yang kamu punya.
  3. Kalau data balik untuk object yang bukan milik store kamu, itu IDOR antar-shop.

Script sederhana untuk enumerasi ID numerik (jalankan dengan python3 -I, hanya terhadap store milik sendiri):

import requests

# Session cookie dari dev store A (punya kamu sendiri)
COOKIES = {"_secure_session_id": "ISI_SESSION_KAMU"}
ENDPOINT = "https://admin.shopify.com/admin/internal/web/graphql/core"

QUERY = """
query BillDetails($id: ID!) {
  billingInvoice(id: $id) {
    id
    totalAmount { amount }
    billingAddress { firstName lastName city }
  }
}
"""

def probe(invoice_id):
    gid = f"gid://shopify/BillingInvoice/{invoice_id}"
    r = requests.post(ENDPOINT, cookies=COOKIES, json={
        "operationName": "BillDetails",
        "variables": {"id": gid},
        "query": QUERY,
    })
    data = r.json().get("data", {}).get("billingInvoice")
    if data:
        print(f"[!] ID {invoice_id} KEMBALI DATA: {data.get('billingAddress')}")
    else:
        print(f"[ ] ID {invoice_id} kosong/ditolak")

# Uji rentang ID di sekitar invoice milik store kamu sendiri
for i in range(1000, 1010):
    probe(i)

Kalau ID yang bukan milik store kamu tetap mengembalikan data, kamu punya IDOR. Prioritaskan ID numerik. Untuk UUID, ingat Shopify nggak opt-in ke standar UUID IDOR HackerOne — kamu harus menunjukkan jalur akses realistis ke UUID itu (misal bocor di response lain), bukan sekadar “bisa ditebak”. Kalau nggak, report-nya N/A — lihat 7 Alasan Report Dapat N/A.

Langkah 3 — Uji permission boundary

Login sebagai staff account low-permission (setup-nya di artikel dev store). Lalu coba jalankan operation yang harusnya butuh permission lebih tinggi. Yang pernah dibayar:

  • Staff tanpa permission baca saldo Shopify Payments via Admin GraphQL (pola #417170).
  • AdminGenerateSessionPayload bocor ke staff no-permission.
  • Staff bisa trigger BULK_OPERATIONS_FINISH webhook tanpa permission.
  • Staff no-permission bisa listen event WebSocket Shopify Ping.

Teknik pembanding dua token: jalankan operation yang sama dengan session owner dan session staff low-permission, lalu bandingkan response.

import requests

ENDPOINT = "https://admin.shopify.com/admin/internal/web/graphql/core"
QUERY = '{"query":"query { shopBillingPreference { currency } ... }"}'

sessions = {
    "owner":  {"_secure_session_id": "SESSION_OWNER"},
    "staff":  {"_secure_session_id": "SESSION_STAFF_MINIMAL"},
}

for role, cookies in sessions.items():
    r = requests.post(ENDPOINT, cookies=cookies, data=QUERY,
                      headers={"Content-Type": "application/json"})
    print(f"=== {role} (status {r.status_code}) ===")
    print(r.text[:300])

Kalau staff low-permission dapat response data sensitif yang sama dengan owner, itu boundary violation. Yang membuat report ini valid menurut criteria Shopify: harus escalate ke Sensitive Permission, atau dampak finansial langsung (misal turunkan harga produk tanpa permission), atau akses Buyer PII yang nggak diotorisasi.

Penting — yang BUKAN bug: admin-vs-staff dalam organisasi yang sama. Admin memang punya otoritas atas staff di org-nya. IDOR valid harus cross-organizational atau privilege escalation nyata.

Langkah 4 — Pivoting GraphQL

Teknik lanjutan: query yang legal di konteks A dipakai untuk mengakses node di konteks B. Ada disclosed report berjudul “Get access to non public information by pivoting with graphql queries” — intinya kamu menyambung relasi antar-node untuk menjangkau data yang seharusnya di luar scope.

Cari field relasi dalam schema yang mengembalikan object terkait. Kadang object induk ter-protect tapi object anak yang diakses via relasi nggak:

query {
  order(id: "gid://shopify/Order/123") {
    # order mungkin ter-proteksi...
    customer {
      # ...tapi relasi customer bocorin PII?
      email
      phone
      defaultAddress { address1 city }
    }
  }
}

Gunakan introspection (di dev store kamu) untuk memetakan semua relasi, lalu cari jalur dari node yang kamu kontrol ke node sensitif.

Langkah 5 — Cross-shop context confusion

Ini pola dari GHSA di shopify_app: current_shopify_domain bisa resolve dari query param shop alih-alih dari ID token terverifikasi. Akibatnya request yang authenticated untuk shop A, tapi dengan param shop=B, bisa beroperasi di konteks shop B.

Kalau kamu build custom app sendiri (legal di dev store kamu), uji: apakah app menentukan konteks shop dari token terverifikasi, atau dari param yang bisa dikontrol attacker?

GET /app/data?shop=victim-store.myshopify.com HTTP/1.1
Authorization: Bearer <token_valid_untuk_attacker_store>

Kalau backend mengembalikan data victim-store padahal token-nya milik attacker store, itu context confusion. Pola ini juga relevan dicari di permukaan app lain.

Langkah 6 — Area app proxy & checkout extension

Backend yang menerima request dari checkout extension atau app proxy sering jadi titik lemah. Docs Shopify sendiri mengakui: server app nggak bisa memastikan request benar-benar dari Shopify. Yang perlu diuji:

  • Backend yang nggak validasi session token sub vs data yang diminta → buyer A minta data buyer B.
  • Parameter pollution di validasi HMAC app proxy (duplikat shop/signature/timestamp) yang bisa desync nilai yang di-sign vs yang dibaca — pola dari GHSA yang udah fixed di library versi baru, jadi cari app yang pakai library lama.
  • App proxy yang nggak cek logged_in_customer_id sesuai data yang diminta.
# Parameter pollution: dua nilai shop, mana yang di-sign vs dibaca?
GET /proxy/data?shop=mine.myshopify.com&shop=victim.myshopify.com&signature=... HTTP/1.1

Common mistakes saat testing GraphQL

  1. Testing di store orang lain. Langsung banned. SELALU di dev store sendiri.
  2. Pakai scanner otomatis. Dilarang program, dan bikin noise yang kena N/A.
  3. Fokus UUID IDOR. Buang waktu — Shopify nggak opt-in standar itu.
  4. Berhenti di “ID bisa di-enumerate”. Harus tunjukin data sensitif apa yang bocor.
  5. Lapor admin-vs-staff same org. Intended behavior, N/A.
  6. Nggak sensor PII di PoC. Bisa bikin report ditolak atau masalah legal.

Report GraphQL yang bagus menyertakan: operation lengkap, ID yang di-swap, data yang bocor (PII disensor), dan dampak eksplisit. Jangan berhenti di “ID bisa di-enumerate” — tunjukkan data sensitif apa yang jadi terekspos. Escalation inilah yang menaikkan payout. Baca juga Cara Dapat Bug Pertama di Shopify HackerOne untuk kerangka report.

Checklist testing GraphQL

  • Dua dev store milik sendiri siap (email @wearehackerone.com).
  • Semua GraphQL operation sudah di-dump dari Network tab.
  • Operation dengan ID numerik + data sensitif diprioritaskan.
  • IDOR diuji dengan swap ID antar-store sendiri.
  • Permission boundary diuji dengan pembanding token owner vs staff.
  • Pivoting lewat relasi node sudah dicoba.
  • Cross-shop context (param shop) sudah diuji di custom app.
  • Impact di-escalate ke PII/finansial, bukan berhenti di anomali.
  • PII pihak lain disensor di PoC.

Checklist siap pakai

Mapping GraphQL operation secara manual makan waktu. Gue sama tim rangkum pola IDOR/permission yang terbukti dibayar, daftar operation yang layak dicek, dan Known Issues yang harus dihindari ke dalam Shopify Bug Bounty Starter Pack ($1). Hasil riset puluhan disclosed report, dirangkum jadi checklist praktis.

GraphQL adalah permukaan paling produktif di Shopify. Kuncinya bukan jalankan scanner (dilarang, lagipula), tapi memahami pola: ID numerik yang nggak di-scope, permission yang nggak konsisten, dan konteks shop yang bisa dikontrol attacker.

Sumber & referensi

  • “Hacking GraphQL at Shopify” — Shopify Engineering blog
  • Shopify Bug Bounty criteria: hackerone.com/shopify
  • Disclosed reports: #2207248 (billing IDOR $5.000), #417170 (staff permission payments)
  • GitHub Security Advisory (GHSA) untuk shopify_app — cross-shop domain resolution
  • OWASP API Security Top 10 — BOLA/IDOR: owasp.org/API-Security