Debug SSR yang membocorkan data via hydration di Next.js pada dasarnya adalah pekerjaan mencari data sensitif yang tidak tampil di UI, tetapi tetap dikirim ke browser saat render awal. Masalah ini berbahaya karena developer sering hanya memeriksa apa yang terlihat di halaman, padahal token, email internal, stack trace, hasil query, atau object error bisa ikut masuk ke HTML SSR, payload props, atau state awal hydration.

Dalam konteks risiko modern seperti yang dibahas pada isu The Memory Heist, pelajaran utamanya bukan sekadar “ada bug baru”, melainkan bahwa data yang sempat berada di memori, state render, atau boundary yang salah dapat terekspos ke tempat yang tidak semestinya. Pada aplikasi Next.js, bentuk yang paling sering terjadi bukan UI rusak, tetapi UI tampak normal sementara browser sudah menerima data yang seharusnya hanya dipakai di server.

Mengapa kebocoran via hydration berbahaya

Pada alur SSR, server merender HTML awal agar halaman cepat tampil dan siap di-hydrate oleh JavaScript di browser. Agar React bisa melanjutkan state yang sama di klien, sebagian data render biasanya perlu diserialisasi. Di sinilah masalah muncul: jika object yang dikirim terlalu besar atau mengandung field sensitif, browser akan tetap menerimanya walaupun field tersebut tidak pernah dirender ke layar.

Akibatnya:

  • Data sensitif bisa terlihat lewat View Source, panel Network, atau data hydration internal.
  • Informasi yang tidak digunakan UI tetap bisa diambil oleh script pihak ketiga, ekstensi browser, atau penyerang dengan akses ke sesi pengguna.
  • Error object, hasil fetch server, atau cache render dapat mengungkap struktur backend, query internal, header, atau identifier rahasia.

Catatan penting: “Tidak tampil di komponen” bukan berarti “tidak terkirim ke klien”. Dalam bug kelas ini, sumber masalah hampir selalu ada pada data yang ikut masuk ke boundary render, bukan pada elemen HTML yang terlihat.

Pola kebocoran yang paling umum di Next.js

1. Mengirim object server mentah ke komponen klien

Ini pola paling sering. Developer mengambil data user atau hasil query lengkap di server, lalu meneruskan seluruh object ke komponen klien hanya karena praktis.

// Tidak aman: seluruh object diteruskan ke client component
export default async function Page() {
  const session = await getServerSession();
  const account = await getAccount(session.userId);

  return <AccountPanel account={account} />;
}

Jika account mengandung field seperti apiKey, internalNotes, billingId, atau metadata lain, field tersebut bisa ikut terserialisasi saat hydration.

Versi yang lebih aman adalah memproyeksikan data minimum yang benar-benar dibutuhkan UI:

// Lebih aman: kirim hanya field yang dibutuhkan UI
export default async function Page() {
  const session = await getServerSession();
  const account = await getAccount(session.userId);

  const publicAccount = {
    id: account.id,
    name: account.name,
    plan: account.plan
  };

  return <AccountPanel account={publicAccount} />;
}

2. Menyebarkan props dengan operator spread tanpa audit

// Tidak aman: sulit melacak field apa saja yang ikut terkirim
return <ProfileCard {...user} />;

Operator spread memudahkan kebocoran karena perubahan skema data di backend otomatis ikut terbawa ke boundary render. Hari ini aman, besok field baru seperti roleDebug atau resetTokenPreview bisa ikut terkirim tanpa disadari.

3. Error object ikut masuk ke state atau props

Saat debugging, developer sering mengirim object error utuh ke komponen agar mudah dilihat. Ini berisiko karena object error dapat memuat stack trace, payload request, response backend, query, bahkan header internal.

// Tidak aman
try {
  const result = await fetchSensitiveData();
  return <Widget data={result} />;
} catch (error) {
  return <ErrorPanel error={error} />;
}

Pendekatan aman adalah mengubah error menjadi bentuk yang sudah disanitasi:

// Lebih aman
try {
  const result = await fetchSensitiveData();
  return <Widget data={result} />;
} catch (error) {
  return <ErrorPanel error={{ message: 'Terjadi kesalahan saat memuat data.' }} />;
}

4. Cache server atau hasil fetch dipakai ulang tanpa boundary yang jelas

Kebocoran juga bisa terjadi ketika hasil fetch, memoization, atau cache berbasis request tidak dibatasi dengan benar. Jika developer menyimpan object kaya data di lapisan cache render, lalu meneruskannya ke komponen yang salah, data sensitif dapat ikut masuk ke hasil SSR.

Masalahnya bukan hanya data bocor ke user yang sama, tetapi juga risiko pencampuran konteks jika strategi cache atau key tidak tepat. Karena itu, data yang bersifat rahasia sebaiknya tidak disimpan dalam bentuk yang siap dirender ke klien.

5. Render mismatch yang mendorong fallback state berbahaya

Hydration mismatch biasanya dianggap bug UI, tetapi kadang gejalanya menutupi isu data. Misalnya server merender state awal tertentu, lalu klien memperbaiki state setelah mount. Jika state awal server mengandung data lebih banyak daripada yang akhirnya dipakai di klien, data sensitif tetap sudah terkirim pada respons awal.

Contoh umum:

  • Server merender object lengkap, klien kemudian memfilter field setelah hydration.
  • Server merender pesan error detail, klien menggantinya menjadi pesan generik.
  • Server mengisi default state dari hasil fetch internal, klien kemudian melakukan normalisasi.

Dalam semua kasus itu, sanitasi terjadi terlalu terlambat karena browser sudah menerima payload awal.

Gejala yang perlu dicurigai saat debugging

UI yang tampak normal tidak berarti aman. Beberapa gejala berikut layak dicurigai:

  • Komponen klien menerima props object yang jauh lebih besar daripada kebutuhan tampilan.
  • Ada logika “sembunyikan field sensitif di klien” alih-alih tidak mengirimkannya sejak awal.
  • Halaman error menampilkan detail teknis saat SSR, lalu berubah setelah hydration.
  • Data dari session, server action, atau hasil query ORM diteruskan mentah ke komponen presentasional.
  • Ada penggunaan JSON.stringify(data) untuk debug di komponen atau page.
  • View Source menampilkan blob JSON besar, meskipun halaman hanya menampilkan sedikit data.

Langkah audit praktis: mulai dari boundary data, bukan dari UI

1. Petakan boundary server dan client component

Audit semua titik tempat data bergerak dari server ke klien:

  • Props dari server component ke client component.
  • Data hasil loader atau fetch yang dipakai untuk render awal.
  • Nilai default form atau state awal yang dibangun di server.
  • Object error, session, user profile, dan response backend.

Pertanyaan yang harus diajukan: field mana yang benar-benar diperlukan browser untuk menggambar UI ini? Jika jawabannya tidak jelas, data terlalu dekat dengan boundary klien.

2. Cari object “gemuk” dan penyebaran props

Lakukan pencarian terhadap pola yang sering menyebabkan kebocoran:

grep -R "{..." app components
grep -R "error={error}" app components
grep -R "session" app components
grep -R "JSON.stringify(" app components

Perintah ini bukan audit sempurna, tetapi cukup efektif untuk menemukan titik rawan yang perlu inspeksi manual.

3. Audit server actions dan fungsi utilitas bersama

Server action atau helper sering mengembalikan object yang terlalu kaya karena awalnya dibuat untuk kebutuhan internal server. Masalah muncul ketika hasilnya dipakai ulang oleh UI.

Periksa:

  • Apakah action mengembalikan record database utuh?
  • Apakah helper mewariskan field internal seperti token, audit trail, flag debug, atau header?
  • Apakah ada object error yang dilempar ulang tanpa sanitasi?

Pisahkan tipe hasil internal dan tipe hasil yang aman untuk render. Jangan memakai satu DTO untuk semua lapisan.

4. Verifikasi di browser: View Source, DevTools, dan Network

Verifikasi nyata harus dilakukan di browser, karena di situlah bukti kebocoran terlihat.

  1. Buka halaman target.
  2. Gunakan View Source, bukan hanya panel Elements. Tujuannya melihat HTML respons awal sebelum dimodifikasi runtime.
  3. Cari kata kunci sensitif: email internal, token, API key, stack trace, nama tabel, field billing, atau identifier privat.
  4. Buka Network dan inspeksi dokumen HTML utama. Cari payload JSON besar atau string sensitif.
  5. Jika ada navigasi berbasis data, inspeksi juga request yang dipakai untuk prefetched payload atau data route terkait.

Jika data sensitif muncul di salah satu tempat ini, berarti browser sudah menerimanya, walaupun React kemudian tidak pernah menampilkannya.

5. Bandingkan data server vs data yang benar-benar dibutuhkan komponen

Salah satu teknik paling efektif adalah menuliskan kontrak props eksplisit untuk komponen klien. Misalnya, jika komponen hanya butuh name dan avatarUrl, maka tipe props harus hanya berisi dua field itu. Ini memaksa developer melakukan minimisasi data lebih awal.

type PublicProfileProps = {
  name: string;
  avatarUrl: string | null;
};

function PublicProfile(props: PublicProfileProps) {
  return (
    <div>
      <span>{props.name}</span>
      {props.avatarUrl ? <img src={props.avatarUrl} alt="avatar" /> : null}
    </div>
  );
}

Pola aman vs tidak aman

Pola tidak aman: serialisasi object domain langsung ke UI

// account memiliki banyak field internal
const account = await getAccount(userId);
return <ClientAccount account={account} />;

Masalahnya:

  • Field sensitif bisa ikut terserialisasi.
  • Perubahan skema backend otomatis memengaruhi permukaan data di klien.
  • Sulit diaudit karena boundary terlalu lebar.

Pola aman: map ke view model publik

const account = await getAccount(userId);

const accountView = {
  id: account.id,
  displayName: account.displayName,
  subscriptionStatus: account.subscriptionStatus
};

return <ClientAccount account={accountView} />;

Mengapa ini bekerja:

  • Field yang boleh keluar dinyatakan eksplisit.
  • Boundary data menjadi kecil dan mudah direview.
  • Skema domain internal bisa berubah tanpa memperluas payload klien.

Pola tidak aman: menaruh rahasia di state render

// Tidak aman: token hanya untuk request server lanjutan
const state = {
  csrfToken,
  internalApiToken,
  formData
};

return <ClientForm initialState={state} />;

Jika nilainya masuk ke state render, besar kemungkinan nilai itu akan tersedia di klien. Rahasia server tidak boleh ditempatkan dalam object yang menjadi bagian dari output render.

Pola aman: pisahkan rahasia dari state render

Simpan rahasia di sisi server, cookie yang sesuai, session, atau mekanisme backend yang memang dirancang untuk otorisasi server-side. Komponen klien hanya menerima data yang diperlukan untuk interaksi UI.

Mitigasi utama yang benar-benar efektif

1. Minimalkan data sebelum menyentuh boundary render

Mitigasi paling penting adalah data minimization. Jangan mengambil object besar lalu berharap bisa “membersihkan nanti”. Bentuk data aman harus dibuat sedekat mungkin dengan sumber server, sebelum diteruskan ke komponen klien.

2. Gunakan boundary komponen dengan disiplin

Anggap client component sebagai area publik. Apa pun yang masuk ke sana harus diasumsikan dapat dilihat browser. Server component boleh memiliki akses ke data lebih luas, tetapi begitu data diteruskan ke boundary klien, audit harus ketat.

3. Sanitasi error dan hasil logging

Jangan pernah merender error mentah. Buat fungsi normalisasi error yang hanya mengembalikan informasi yang aman untuk pengguna akhir.

function toPublicErrorMessage() {
  return { message: 'Terjadi kesalahan. Silakan coba lagi.' };
}

Detail teknis tetap disimpan di log server, bukan di props render.

4. Hindari serialisasi object kompleks yang tidak perlu

Object dari ORM, response HTTP, class instance, atau object error sering membawa metadata tambahan yang tidak terlihat. Bahkan jika serialisasi otomatis membuang sebagian field, jangan bergantung pada perilaku itu sebagai mekanisme keamanan. Buat object baru yang eksplisit.

5. Pisahkan DTO internal dan DTO publik

Jangan gunakan satu tipe data yang sama untuk database, service internal, dan UI. Pisahkan:

  • model domain/internal,
  • hasil service internal,
  • view model publik untuk render.

Pemisahan ini mengurangi risiko field sensitif ikut lolos karena reuse yang berlebihan.

6. Tinjau strategi cache dan hasil fetch

Pastikan data yang di-cache untuk kebutuhan server tidak otomatis menjadi data yang layak dikirim ke klien. Jika Anda menyimpan object hasil query, map dulu ke bentuk publik ketika benar-benar hendak dirender.

7. Uji halaman error, loading, dan fallback

Banyak kebocoran justru terjadi di jalur yang jarang diuji: error state, fallback loading, empty state, atau kondisi mismatch. Audit semua jalur render, bukan hanya jalur sukses.

Checklist verifikasi sebelum rilis

  • Apakah setiap client component menerima props minimal dan eksplisit?
  • Apakah ada object database, session, atau error yang diteruskan mentah?
  • Apakah ada field rahasia yang hanya “disembunyikan” di klien, bukan dihapus di server?
  • Apakah View Source bebas dari token, stack trace, email internal, identifier privat, dan metadata backend?
  • Apakah respons HTML dan request data di DevTools Network sudah diperiksa?
  • Apakah halaman error dan fallback sudah diuji dengan data nyata?
  • Apakah helper bersama dan server actions mengembalikan DTO publik, bukan object internal?
  • Apakah perubahan skema backend tidak otomatis memperluas props UI lewat spread?

Kesalahan umum saat memperbaiki bug ini

“Data tidak tampil, jadi aman”

Salah. Jika data ada di HTML atau payload hydration, browser tetap sudah menerimanya.

“Nanti dibuang di useEffect setelah mount”

Terlambat. Data sudah terkirim pada respons awal.

“Ini hanya untuk debugging sementara”

Banyak kebocoran produksi berasal dari field debug yang lupa dibersihkan. Debugging data di UI harus dianggap jalur berisiko tinggi.

“Object ini aman karena berasal dari library tepercaya”

Masalahnya bukan hanya sumber object, tetapi field apa yang ikut menyeberang ke klien. Library tepercaya tidak otomatis berarti object-nya aman dirender.

Penutup

Debug SSR yang membocorkan data via hydration di Next.js bukan sekadar mencari mismatch visual, tetapi mengaudit apa yang benar-benar dikirim ke browser. Dalam banyak kasus, UI tampak baik-baik saja, namun props awal, state hydration, object error, atau cache render sudah lebih dulu mengekspos data sensitif.

Pendekatan yang paling efektif adalah sederhana tetapi disiplin: minimalkan data, kecilkan boundary komponen klien, sanitasi serialisasi, dan jangan pernah menaruh rahasia di state render. Jika ragu, buka View Source dan Network lalu cari buktinya. Keamanan SSR sering gagal bukan karena data dipakai di UI, tetapi karena data ikut terbawa saat render awal.