Mobile Dev Talk #24 · MDG Community
Scrape Data from Mobile Apps
…dan cara menjaga app kita sendiri dari hal yang sama.
Nanda Apriliawan
React Native Developer
Tentang saya
Halo, saya Nanda
- React Native Developer
- Pernah nulis soal security & API di Medium (@naandalist)
medium.com/@naandalist · mobiledev.id/@nanda
Sebelum mulai
Disclaimer
- Materi ini untuk belajar, debugging app sendiri, dan memperkuat keamanan.
- Praktikkan ke app milik sendiri, atau yang kamu punya izin.
- Cek Terms of Service & aturan hukum (mis. UU ITE, UU PDP) sebelum menyentuh app orang lain.
- Data pribadi orang lain = jangan disentuh.
Agenda
Perjalanan kita hari ini
01 Scraping itu apa? Praktik di web
02 Kenapa mobile app beda
03 Cara-cara umum scraping mobile app
04 POV pemilik app: apa ruginya?
05 Hardening app dengan cara "unik"
06 Open session: bawa kasusmu
01
Bagian 01
Scraping itu apa?
…dan seperti apa di web
Definisi
Scraping = mengambil data dari sebuah sistem lewat jalur yang dipakai manusia/app,
bukan lewat jalur resmi untuk developer.
API resmi
Ada docs, API key, kontrak, rate limit jelas.
Crawling
Menjelajah link → menemukan halaman (mis. mesin pencari).
Scraping
Mengekstrak isi halaman/response jadi data terstruktur.
Scraping di web
Tiga level scraping web
1. Parse HTML
Fetch halaman → ambil elemen pakai selector.
cheerio, BeautifulSoup
2. Headless browser
Untuk halaman yang dirender JavaScript (SPA).
Playwright, Puppeteer
3. "Hidden API"
Buka DevTools → tab Network → panggil langsung JSON-nya.
fetch, curl
Level 3 adalah jembatan ke mobile: jangan scrape tampilannya, ambil datanya.
Scraping di web
Contoh: parse HTML
import * as cheerio from 'cheerio';
const html = await (await fetch('https://toko-contoh.id/produk')).text();
const $ = cheerio.load(html);
const products = $('.product-card').map((_, el) => ({
name: $(el).find('.title').text().trim(),
price: $(el).find('.price').text().replace(/\D/g, ''),
})).get();
console.log(products);
Gampang… sampai desainer mengganti .product-card
Scraping di web
Kenapa web relatif mudah?
- DevTools ada di setiap browser, Network tab langsung kelihatan
- Kode frontend (JS) terkirim ke client dan bisa dibaca
- Browser percaya CA sistem, tidak ada certificate pinning
- Request bisa di-copy as cURL dalam satu klik
Di mobile, hampir semua kemudahan ini hilang.
02
Bagian 02
Kenapa mobile app beda?
Insight utama
Setiap app di HP kamu sedang ngobrol dengan server.
Di balik layar ada API yang mengirim data mentah, app cuma menggambarnya.
Keterbatasan
Tantangan di mobile
Tidak ada DevTools
Traffic tidak bisa dilihat langsung, butuh proxy di tengah.
TLS & trust store
Sejak Android 7, app tidak percaya user CA secara default.
Certificate pinning
App hanya mau bicara dengan sertifikat tertentu, proxy ditolak.
Kode terkompilasi
APK/IPA, R8/ProGuard, Hermes bytecode: tidak semudah view source.
Request signing
Header dinamis (signature, timestamp, device id) yang harus dihitung.
Attestation & deteksi
Play Integrity, App Attest, deteksi root/jailbreak/emulator.
Plot twist
Tapi… API mobile sering lebih "ramah"
- Response-nya JSON bersih, tidak perlu parse HTML
- Endpoint versi lama sering masih hidup demi backward compatibility
- Asumsi "ini kan cuma dipanggil app kita" → validasi & rate limit lebih longgar
- API key / secret kadang tertanam di binary
Sebagai pemilik app: asumsi "hanya app kita yang memanggil" itu yang paling berbahaya.
03
Bagian 03
Cara-cara umum scraping mobile app
Peta teknik
Lima pendekatan, dari mudah ke sulit
| # | Pendekatan | Inti | Contoh tools |
| 1 | Cek versi web dulu | Sering pakai API yang sama | DevTools |
| 2 | Intercept traffic | Proxy di antara app & server | mitmproxy, Charles, Proxyman, HTTP Toolkit |
| 3 | Static analysis | Bongkar APK, cari URL/key/logic | jadx, apktool |
| 4 | Dynamic instrumentation | Hook fungsi saat app jalan | Frida, objection |
| 5 | UI automation | "Pencet" app seperti manusia | Appium, Maestro |
Teknik #2 · Intercept traffic
Menonton obrolan app dan server
- Jalankan proxy di laptop, arahkan Wi-Fi HP/emulator ke sana
- Pasang CA sertifikat proxy di device (emulator / device test)
- Buka layar yang datanya kamu mau → lihat request yang muncul
Untuk app sendiri: build debug dengan network_security_config yang trust user CA, tanpa perlu bypass apa pun.
Teknik #2 · Membaca request
Anatomi sebuah request
GET /v2/products?category=12&page=1&limit=20 HTTP/2
Host: api.example-app.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
X-App-Version: 5.12.0
X-Device-Id: 8f3c2a91-...
X-Signature: 9b1e0c... <-- dinamis? dihitung dari apa?
User-Agent: okhttp/4.12.0
{ "data": [ ... ], "meta": { "next_cursor": "eyJpZCI6..." } }
- Endpoint di balik layar mana? Cocokkan dengan aksi di UI.
- Header mana yang wajib? Hapus satu per satu sampai request gagal.
- Token dari mana & kapan expired? Cari request login / refresh.
Teknik #2 · Replay
Rebuild request di luar app
const headers = {
Authorization: `Bearer ${TOKEN}`,
'X-App-Version': '5.12.0',
'User-Agent': 'okhttp/4.12.0',
};
const res = await fetch(
'https://api.example-app.com/v2/products?category=12&page=1',
{ headers },
);
console.log(await res.json());
Kalau ini berhasil… server tidak bisa membedakan script ini dengan app asli.
Teknik #2 · Data nyata itu berantakan
Pagination & response yang "messy"
let cursor = null;
const all = [];
do {
const url = new URL(BASE + '/v2/products');
if (cursor) url.searchParams.set('cursor', cursor);
const { data, meta } =
await (await fetch(url, { headers })).json();
all.push(...data);
cursor = meta?.next_cursor;
await sleep(1000); // sopan ke server
} while (cursor);
Yang sering bikin pusing
- offset vs cursor vs page token
- Data duplikat saat list berubah
- Field
null / beda tipe antar item
- Nested response, ID dirujuk ke endpoint lain
- Rate limit → HTTP 429
- Token expired di tengah jalan
Teknik #3 · Static analysis
Membongkar APK
- APK itu cuma file ZIP
jadx → Java/Kotlin yang bisa dibaca
- Cari:
https://, api_key, secret, HmacSHA256
- Lihat bagaimana header signature dihitung
Catatan untuk dev React Native
assets/index.android.bundle berisi seluruh JS app kita.
Tanpa Hermes → plain JS (minified).
Dengan Hermes → bytecode, tapi tetap bisa di-decompile.
String seperti API_KEY tetap terbaca.
Teknik #4 & #5
Kalau static tidak cukup
Dynamic instrumentation
Frida menyuntikkan script ke proses app yang sedang jalan.
- Intip argumen & return value fungsi
- Lihat data sebelum dienkripsi / ditandatangani
- Mematikan pengecekan di sisi client
Butuh device root/jailbreak atau app yang di-repackage.
UI automation
Appium/Maestro + baca view hierarchy / OCR.
- Tidak perlu tahu API sama sekali
- Lolos signing & pinning karena pakai app asli
- Tapi lambat, rapuh, mahal di-scale
Realistis
Kapan sulit, dan kapan tidak worth it?
Mulai sulit kalau…
- Pinning + deteksi root/Frida
- Request signing yang berubah per versi app
- Play Integrity / App Attest diverifikasi server
- Protobuf tanpa schema
- Rate limit per device + per akun
Tidak worth it kalau…
- Ada API resmi / partner program
- Data bisa dibeli atau diminta baik-baik
- Melanggar ToS / menyentuh data pribadi
- Biaya maintenance > nilai datanya
04
Bagian 04
Balik kursi: POV pemilik app
"API saya di-hit dari luar app. Terus kenapa?"
Kerugian
Apa ruginya kalau API kita "dicuri"?
Biaya
- Tagihan server & bandwidth naik
- API pihak ketiga yang kita bayar per call (AI, maps, OCR) ikut dipakai orang
- Abuse OTP SMS/WhatsApp = bakar saldo
Bisnis
- Katalog, harga, stok disedot kompetitor
- Abuse promo / voucher / referral
- Konten eksklusif bocor
User asli
- Server lambat karena traffic bot
- Rate limit ikut kena user asli
Data analitik kacau
- Keputusan produk diambil dari data yang salah
- Ini yang paling sering tidak disadari →
"Tapi backend saya sudah secure…"
Auth yang benar ≠ client yang sah
- Backend sudah cek token, authorization, validasi input
- Tapi token itu milik akun yang sah, scraper cukup login seperti user biasa
- Server tahu SIAPA usernya, tapi tidak tahu DARI MANA request-nya
Request yang "valid" secara auth tetap bisa merugikan kalau tidak datang dari app kita.
Studi kasus · ilustrasi
Ketika dashboard "Tidak Sync"
75%
request ke server tidak punya jejak di app
Request GET /products di server | 1.200.000 |
Event product_list_viewed di analytics | 300.000 |
Efek domino
- Conversion rate terlihat anjlok
- Hasil A/B test bias
- "Produk populer" ternyata cuma sering di-scrape
- Rekomendasi & ranking ikut rusak
- Capacity planning salah
Angka di atas ilustrasi. Kebalikannya juga terjadi: bot yang menembak endpoint tracker → DAU palsu.
05
Bagian 05
Hardening app kita
Tujuannya bukan 100% aman, tapi menaikkan biaya penyerang
Defense in depth
Berlapis, bukan satu tembok
L1HTTPS + certificate pinningnetwork
L2Obfuscation: R8/ProGuard, Hermes, secret tidak disimpan plainclient
L3Request signing berbasis waktu (HMAC) ← fokus kitaclient + server
L4App attestation: Play Integrity / App Attestplatform
L5Deteksi lingkungan: root, jailbreak, emulator, Fridaclient
L6Rate limit, anomaly detection, force updateserver
Masalahnya
Static API key = golden ticket abadi
- "Kan sudah HTTPS?" Belum cukup: HTTPS bisa dibuka di device yang dipasangi rogue root CA (persis teknik #2 tadi)
- Key statis tidak pernah expired → sekali bocor, bocor selamanya
- Butuh solusi yang ringan: tanpa database, cocok untuk proxy/microservice
Ide
Time-based signature dengan HMAC
Jangan kirim secret-nya. Kirim bukti bahwa kamu tahu secret-nya,
dan bukti itu kedaluwarsa dalam hitungan detik.
signature = HMAC_SHA256( secret, epochMs + requestUrl + bodyString )
Shared secret
Ada di client & server, tidak pernah dikirim lewat jaringan.
⏱️ Epoch UTC
Sama di seluruh dunia. Membuat signature basi dengan cepat.
URL + body
Signature terikat ke request ini. Ubah 1 karakter → invalid.
Alur
Bagaimana client & server bekerja
Client
- Ambil
epoch = Date.now()
- Susun
epoch + url + body
- Hitung HMAC-SHA256 dengan secret
- Kirim header
X-Auth-Epoch & X-Auth-Signature
Server
- Header lengkap? Kalau tidak → 401
|now − epoch| > 60 dtk? → 401 (basi)
- Susun ulang string dengan urutan sama persis
- Hitung signature sendiri → bandingkan
- Cocok → teruskan ke API tujuan
Simulasi
Disadap lalu di-replay? Cepat basi.
Bandingkan dengan static key tadi: copy yang sama hanya berguna beberapa detik, dan request yang diubah langsung ditolak.
Implementasi · client (React Native)
Menandatangani request
import { createHmac } from 'react-native-quick-crypto';
const SECRET = getSecretFromNative(); // jangan hardcode string polos di JS
export async function signedFetch(url: string, body?: object) {
const epoch = Date.now().toString();
const bodyStr = body ? JSON.stringify(body) : '';
const signature = createHmac('sha256', SECRET)
.update(`${epoch}${url}${bodyStr}`)
.digest('hex');
return fetch(url, {
method: body ? 'POST' : 'GET',
headers: {
'Content-Type': 'application/json',
'X-Auth-Epoch': epoch,
'X-Auth-Signature': signature,
},
body: body ? bodyStr : undefined,
});
}
Implementasi · server (Hono middleware)
Memverifikasi signature
const SKEW_MS = 60_000;
app.use('/api/protected/*', async (c, next) => {
const epoch = Number(c.req.header('X-Auth-Epoch'));
const sig = c.req.header('X-Auth-Signature') ?? '';
if (!epoch || Math.abs(Date.now() - epoch) > SKEW_MS)
return c.json({ error: 'Request expired' }, 401);
const body = await c.req.raw.clone().text();
const expected = createHmac('sha256', SECRET)
.update(`${epoch}${c.req.url}${body}`)
.digest('hex');
if (sig.length !== expected.length ||
!timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
return c.json({ error: 'Invalid signature' }, 401);
await next(); // lolos → proxy ke API tujuan
});
Hasil
Apa yang kita dapat, dan apa yang tidak
Didapat
- Request yang disadap basi dalam ±60 detik
- Request tidak bisa diubah (URL/body terikat)
- Secret tidak pernah lewat jaringan
- Stateless: tanpa database, cocok untuk proxy
- Copy-as-cURL dari proxy tidak lagi cukup
Keterbatasan
- HTTPS tetap wajib, ini lapisan tambahan
- Secret tetap ada di app → bisa diekstrak oleh reverse engineer yang niat
- Replay masih mungkin di dalam jendela 60 detik
- Bukan pengganti user auth (OAuth/PKCE)
Inspirasi · Google Maps API key
Hanya app terdaftar yang boleh akses?
Key dibatasi ke package name + SHA-1 sertifikat signing (iOS: bundle ID). App lain ditolak.
Plot twist
Kenapa script tadi bisa lolos?
- App mengirim header
X-Android-Package & X-Android-Cert (iOS: X-Ios-Bundle-Identifier)
- Nilainya bukan rahasia: package ada di manifest, SHA-1 terbaca lewat
apksigner verify --print-certs
- Server hanya mencocokkan string, jadi siapa pun yang menyalin header ikut lolos
Menyebut fingerprint ≠ membuktikan fingerprint.
Versi yang kuat
App attestation: bukti dari Google & Apple
Play Integrity (Android) · App Attest (iOS) · atau Firebase App Check yang membungkus keduanya.
Implementasi · Play Integrity + time-based signature
Ikat token ke request, verifikasi di server
// App (pseudo-code): token diikat ke request ini
const requestHash = sha256(`${epoch}${url}${body}`);
const token = await integrity.requestToken({ requestHash });
// Server: decode lewat Google, lalu cek isinya
const { tokenPayloadExternal: v } =
await playintegrity.v1.decodeIntegrityToken({
packageName: 'com.contoh.app',
requestBody: { integrityToken: token },
});
const valid =
v.requestDetails.requestHash === sha256(`${epoch}${url}${body}`) &&
Date.now() - Number(v.requestDetails.timestampMillis) < 60_000 &&
v.appIntegrity.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
v.appIntegrity.certificateSha256Digest.includes(OUR_CERT_SHA256) &&
v.deviceIntegrity.deviceRecognitionVerdict?.includes('MEETS_DEVICE_INTEGRITY');
Catatan praktis
Sebelum dipasang di production
Biaya & performa
- Ada kuota & latency: jangan attest setiap request
- Attest saat login / awal sesi, lalu server terbitkan token pendek (mis. 10 menit)
- Endpoint sensitif (checkout, OTP) boleh attest per request
Kasus pinggiran
- Device tanpa Google Play Services, emulator dev: siapkan fallback & mode debug
- App Attest hanya di device fisik iOS
- Root + hooking canggih tetap mungkin: rate limit & monitoring tetap wajib
Level up
Membuatnya lebih sulit lagi
- Nonce + Redis TTL 60 dtk → setiap request hanya sekali pakai
- Sembunyikan secret di native layer (JNI/C++), dipecah & dirakit saat runtime
- Rotasi secret per versi app + force update versi lama
- Gabungkan dengan attestation: secret/token hanya diberikan ke device yang lolos Play Integrity / App Attest
- Signature gagal = sinyal, bukan cuma 401: log & pantau polanya
- Canary data: sisipkan item jebakan di response → kalau muncul di tempat lain, kamu tahu bocornya dari mana
- Rekonsiliasi analytics: bandingkan API call vs event tracker per endpoint
- Response kosong yang "sopan" untuk bot, ketimbang error yang memberi petunjuk
Takeaways
Yang perlu dibawa pulang
- App mobile hanya menggambar data, API-nya yang jadi target.
- Semua yang ada di client bisa dibaca dan diubah.
- Backend yang "secure" belum tentu aman dari kerugian bisnis; analytics pun bisa kacau.
- Security itu soal menaikkan biaya penyerang, berlapis-lapis.
- Time-based signature: murah, stateless, dan langsung mematikan replay sederhana.
- Mau API hanya untuk app terdaftar? Pakai attestation, bukan sekadar header fingerprint.
06
Bagian 06
Open session
Bawa app atau request yang bikin kamu stuck,
kita bedah bareng.
(app sendiri / yang kamu punya izin, ya)
Terima kasih!
Nanda Apriliawan
medium.com/@naandalist · mobiledev.id/@nanda
Bacaan: "Hardening Your Static API Keys with Time-Based Signatures" (Medium)