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

7 November 2026 · Online
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

#PendekatanIntiContoh tools
1Cek versi web duluSering pakai API yang samaDevTools
2Intercept trafficProxy di antara app & servermitmproxy, Charles, Proxyman, HTTP Toolkit
3Static analysisBongkar APK, cari URL/key/logicjadx, apktool
4Dynamic instrumentationHook fungsi saat app jalanFrida, objection
5UI automation"Pencet" app seperti manusiaAppium, Maestro
Teknik #2 · Intercept traffic

Menonton obrolan app dan server

  1. Jalankan proxy di laptop, arahkan Wi-Fi HP/emulator ke sana
  2. Pasang CA sertifikat proxy di device (emulator / device test)
  3. 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 server1.200.000
Event product_list_viewed di analytics300.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

  1. Ambil epoch = Date.now()
  2. Susun epoch + url + body
  3. Hitung HMAC-SHA256 dengan secret
  4. Kirim header X-Auth-Epoch & X-Auth-Signature

Server

  1. Header lengkap? Kalau tidak → 401
  2. |now − epoch| > 60 dtk? → 401 (basi)
  3. Susun ulang string dengan urutan sama persis
  4. Hitung signature sendiri → bandingkan
  5. 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

  1. App mobile hanya menggambar data, API-nya yang jadi target.
  2. Semua yang ada di client bisa dibaca dan diubah.
  3. Backend yang "secure" belum tentu aman dari kerugian bisnis; analytics pun bisa kacau.
  4. Security itu soal menaikkan biaya penyerang, berlapis-lapis.
  5. Time-based signature: murah, stateless, dan langsung mematikan replay sederhana.
  6. 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)