Next.js: API Key yang Bocor ke Browser dan Cara Menutupnya
Aplikasi Next.js yang dibangun cepat, apalagi dengan bantuan AI, sering memanggil layanan pihak ketiga langsung dari komponen di browser. Kodenya berjalan, fiturnya muncul, dan tidak ada error. Masalahnya baru terlihat kalau seseorang membuka DevTools: kunci API ikut terkirim ke setiap pengunjung, tertanam di berkas JavaScript yang bisa diunduh siapa saja.
Artikel ini menjelaskan kapan nilai rahasia di Next.js berpindah ke browser, cara membuktikannya sendiri dalam sepuluh menit, pola yang benar untuk memanggil API berbayar, dan apa yang harus dilakukan bila kunci sudah terlanjur terekspos.
Aturan dasar: variabel server vs NEXT_PUBLIC_
Next.js membedakan dua jenis variabel lingkungan. Perbedaan ini menentukan apakah sebuah nilai tinggal di server atau ikut dikirim ke browser.
| Jenis variabel | Di mana bisa dibaca | Ikut ke bundle browser? |
|---|---|---|
OPENAI_API_KEY (tanpa awalan) | Kode server: Route Handler, Server Component, Server Action, API Route | Tidak |
NEXT_PUBLIC_OPENAI_API_KEY | Server dan browser | Ya — nilainya ditulis langsung ke JavaScript saat build |
Properti env di next.config.js | Server dan browser | Ya — juga disisipkan saat build |
Kata kuncinya: disisipkan saat build. Variabel berawalan NEXT_PUBLIC_ bukan dibaca saat halaman dibuka; nilainya sudah menjadi teks biasa di dalam berkas /_next/static/chunks/*.js. Menghapus variabel itu dari dashboard hosting tanpa build ulang tidak mengubah apa pun pada berkas yang sudah terbit.
Lima jalur kunci API bocor ke browser
- Awalan
NEXT_PUBLIC_pada kunci rahasia. Paling umum. Sering terjadi karena pesan error "variable is undefined" di komponen client, lalu solusinya menambah awalan supaya error hilang. - Memanggil API berbayar langsung dari komponen
"use client". Untuk memanggil OpenAI, Stripe, atau layanan email dari browser, kuncinya harus ada di browser. Tidak ada cara aman untuk itu. - Properti
envdinext.config.js. Nilai di sini disisipkan ke bundle sepertiNEXT_PUBLIC_, walau namanya tidak berawalan apa pun. - Modul server ikut terimpor ke komponen client. Berkas utilitas yang membaca kunci dari
process.envlalu diimpor oleh komponen client bisa menyeret konfigurasi yang tidak seharusnya ke bundle. - Source map produksi. Bila
productionBrowserSourceMapsdinyalakan, berkas.mapmemperlihatkan kode sumber asli, termasuk konstanta yang ditulis langsung di kode.
Kunci mana yang memang boleh ada di browser
Tidak semua kunci di browser adalah kebocoran. Beberapa layanan memang dirancang memakai kunci publik, asalkan pengamannya ada di tempat lain.
| Boleh di browser (dengan syarat) | Tidak boleh di browser |
|---|---|
Stripe publishable key pk_live_… | Stripe secret key sk_live_… |
| Supabase anon key — hanya bila Row Level Security aktif dan benar | Supabase service_role key (melewati semua RLS) |
Konfigurasi web Firebase (apiKey) — keamanan ada di Security Rules | Kunci OpenAI / Anthropic sk-… |
| Google Maps key yang dibatasi HTTP referrer | AWS access key, token GitHub, URL database berisi password |
Untuk kolom kiri, kunci yang terlihat bukan masalah, tetapi aturan di belakangnya wajib diperiksa. Lihat apakah Supabase aman untuk produksi dan Firebase rules allow all.
Cara membuktikan sendiri dalam 10 menit
1. DevTools: cari di semua berkas yang dimuat
Buka situs produksi, tekan F12, buka tab Sources, lalu tekan Ctrl+Shift+F (Mac: Cmd+Option+F) untuk mencari di semua berkas. Cari pola berikut satu per satu: sk-, sk_live_, AKIA, service_role, ghp_. Buka juga beberapa halaman lain di situs Anda, karena Next.js memuat potongan JavaScript per halaman: kunci bisa saja hanya ada di halaman dashboard atau checkout.
2. Tab Network: siapa yang memanggil API berbayar
Di tab Network, pakai fitur aplikasi yang memanggil AI atau pembayaran. Jika ada permintaan langsung dari browser ke api.openai.com, api.anthropic.com, atau api.stripe.com dengan header Authorization, kuncinya pasti ada di browser. Yang benar: browser memanggil endpoint di domain Anda sendiri, lalu server Anda yang meneruskan.
3. Folder hasil build
Di mesin pengembangan, jalankan build produksi lalu cari di berkas statis yang akan dikirim ke browser:
npm run build
grep -rEo "sk-[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}" .next/static | head
Hasil kosong adalah tanda baik untuk pola itu, tetapi bukan jaminan untuk semua jenis kunci. Tambahkan pola layanan lain yang Anda pakai.
4. Daftar variabel yang berawalan NEXT_PUBLIC_
grep -rn "NEXT_PUBLIC_" --include=*.ts --include=*.tsx --include=*.js . | grep -v node_modules
Periksa setiap nama yang muncul: apakah nilainya memang publik menurut tabel di atas?
Pola yang benar: Route Handler sebagai perantara
Pindahkan pemanggilan API berbayar ke server. Kunci tetap di variabel tanpa awalan, dan browser hanya berbicara dengan endpoint milik Anda.
// app/api/chat/route.ts — berjalan di server
import "server-only";
export async function POST(req: Request) {
const { message } = await req.json();
// batasi input + wajibkan sesi login sebelum memakai kuota berbayar
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: String(message).slice(0, 2000) }],
}),
});
return new Response(res.body, { status: res.status });
}
Tiga hal yang sering terlupa setelah memindahkan kunci:
- Paket
server-only. Impor"server-only"di modul yang membaca rahasia. Build akan gagal bila modul itu tidak sengaja diimpor oleh komponen client. - Autentikasi dan batas laju. Endpoint perantara tanpa login dan tanpa batas laju hanya memindahkan masalah: siapa pun bisa menghabiskan kuota Anda lewat endpoint itu. Lihat cara uji endpoint API tanpa login.
- Batasi input. Panjang pesan, model yang boleh dipilih, dan jumlah token keluaran ditentukan server, bukan browser.
Sudah terlanjur bocor? Urutannya penting
- Rotasi kunci lebih dulu di dashboard penyedia layanan, lalu pasang kunci baru di variabel server. Memperbaiki kode tanpa rotasi tidak cukup: kunci lama sudah terkirim ke setiap pengunjung sebelumnya.
- Periksa deployment lama. Beberapa platform hosting menyimpan setiap deployment dengan URL tersendiri, termasuk preview. Berkas lama yang masih memuat kunci tetap bisa diakses sampai deployment itu dihapus atau dilindungi.
- Periksa pemakaian. Lihat log tagihan dan pemakaian di penyedia layanan sejak tanggal kunci pertama kali terbit.
- Build ulang dan cek lagi dengan langkah DevTools di atas.
Langkah rotasi per penyedia dibahas di cara rotasi API key yang bocor.
Cek otomatis tanpa install
Free URL scan AISG memeriksa HTML halaman yang Anda masukkan, hingga tiga berkas JavaScript pertama yang dimuat dari domain yang sama, dan source map-nya, untuk pola kunci umum seperti OpenAI, Anthropic, Stripe secret, AWS, GitHub, Slack, dan Google. Nilai yang ditemukan selalu disamarkan di laporan. Batasnya perlu diketahui: potongan JavaScript yang baru dimuat di halaman lain tidak ikut diperiksa, jadi pencarian manual di DevTools tetap perlu untuk halaman dashboard dan checkout. Hasil kosong berarti pola itu tidak terdeteksi di berkas tersebut, bukan jaminan aman.
Ringkasnya: kunci rahasia di Next.js aman selama ia tidak pernah diberi awalan NEXT_PUBLIC_, tidak dipanggil dari komponen client, dan endpoint perantaranya dilindungi login serta batas laju.