Mengamankan Next.js Server Actions: Auth, IDOR & Validasi Zod

Foto oleh FlyD on Unsplash
Tidak. Setiap fungsi use server yang diekspor dikompilasi menjadi endpoint POST publik yang dapat dijangkau lewat permintaan langsung, bukan hanya melalui UI Anda. Next.js menambahkan ID aksi terenkripsi dan pemeriksaan CSRF Origin/Host, tetapi itu bukan otorisasi. Anda harus memverifikasi autentikasi dan otorisasi di dalam setiap aksi sendiri.
Tidak. Tipe dan skema Zod sisi klien hanya berjalan di browser, yang sepenuhnya dikendalikan penyerang. Mereka bisa membaca ID aksi dan mengulang POST dengan payload apa pun. Semua validasi yang penting untuk keamanan harus berjalan di sisi server di dalam aksi, biasanya dengan Zod safeParse di batas masukan.
Autentikasi saja tidak cukup, karena pengguna login mana pun bisa mengirim ID catatan milik orang lain. Setelah autentikasi, muat baris target dari basis data dan pastikan ia milik pemanggil (misalnya memeriksa post.authorId sama dengan ID pengguna sesi) sebelum melakukan mutasi. Jika tidak, lemparkan Forbidden.
Tidak. Redirect halaman atau pemeriksaan proxy hanya mengatur UI mana yang dirender. Server Action adalah titik masuk terpisah dan harus memverifikasi ulang pemanggil sendiri. Dokumentasi Next.js menyebut pemeriksaan autentikasi di dalam aksi sebagai kritis justru karena alasan ini.
CVE-2025-66478 (React2Shell) adalah cacat remote code execution CVSS 10.0 yang diumumkan pada 3 Desember 2025, berasal dari deserialisasi tidak aman pada protokol React Server Components. Ia terdampak pada Next.js 15.x, 16.x, dan build canary sejak 14.3.0-canary.77 yang memakai App Router. Tidak ada solusi sementara; perbarui ke rilis ter-patch seperti 15.5.7 atau 16.0.7 dan rotasi rahasia.

Foto oleh FlyD on Unsplash
Ringkasan Utama
Setiap Next.js Server Action yang ditandai use server dikompilasi menjadi endpoint POST publik yang bisa dipanggil siapa saja secara langsung, jadi tipe TypeScript dan validasi sisi klien tidak melindungi apa pun. Tiap aksi butuh autentikasi sendiri, otorisasi kepemilikan sumber daya untuk mencegah IDOR, validasi Zod sisi server di batas masukan, serta rate limiting pada operasi mahal.
Pertama kali saya merilis Server Action, rasanya seperti memanggil fungsi lokal. Saya mengimpornya, menyerahkannya ke form action, dan mutasinya langsung jalan. Ergonomi itulah jebakannya. Server Action bukan fungsi privat yang hanya bisa dijangkau UI Anda. Ia dikompilasi menjadi endpoint HTTP nyata, dan dokumentasi Next.js menyatakannya dengan gamblang: begitu sebuah aksi dibuat dan diekspor, ia dapat dijangkau lewat permintaan POST langsung, bukan hanya melalui UI aplikasi Anda.
Fakta tunggal itu mengubah seluruh model keamanan. Penyerang tidak pernah menyentuh komponen React Anda. Mereka membuka tab jaringan, membaca ID aksi, dan mengulang POST dengan payload apa pun yang mereka mau. Tanda tangan TypeScript Anda, tombol submit yang dinonaktifkan, skema Zod sisi klien, rendering kondisional yang menyembunyikan tombol dari non-admin, semuanya berjalan di browser, dan browser adalah satu tempat yang tidak Anda kendalikan. Tulisan ini adalah cara saya memperkuat setiap aksi yang saya tulis.
Next.js memang menambahkan proteksi nyata. Ia menghasilkan ID aksi yang terenkripsi dan non-deterministik agar klien tidak bisa menebaknya, menghitung ulang ID itu antar-build, dan dead-code elimination membuang aksi yang tidak pernah Anda referensikan sehingga tak pernah menjadi endpoint yang bisa dipanggil. Server Actions juga hanya menerima POST dan membandingkan header Origin dengan header Host, yang memblokir sebagian besar CSRF di browser modern, terlebih dengan cookie SameSite sebagai bawaan.
Tetapi semua itu bukan otorisasi. Dokumentasi menyatakannya jelas: ini mengurangi risiko ketika lapisan auth tidak ada, namun Anda tetap harus memperlakukan Server Actions sebagai dapat dijangkau lewat permintaan POST langsung dan memverifikasi autentikasi serta otorisasi di dalam masing-masing aksi. ID terenkripsi menghentikan penebakan, bukan pengguna yang sudah login mengulang permintaan untuk sumber daya yang bukan miliknya. Batas keamanan adalah tubuh aksi, dan tidak ada satu pun sebelum itu yang boleh dipercaya.
Model berpikir yang melekat pada saya: perlakukan setiap fungsi use server dengan kecurigaan yang sama seperti rute REST publik. Jika Anda tidak akan mengeksposnya sebagai endpoint API tanpa autentikasi, jangan merilisnya sebagai Server Action tanpa pemeriksaan yang sama. Sintaks panggilan fungsi yang praktis itu adalah fitur UI, bukan fitur keamanan.
Seiring waktu saya menetapkan urutan tetap di bagian atas setiap aksi yang melakukan mutasi. Melewatkan salah satunya meninggalkan celah nyata, dan urutannya penting karena tiap langkah bergantung pada langkah sebelumnya.
Berikut pola yang saya andalkan, diadaptasi langsung dari panduan data-security Next.js. Ketiga pemeriksaan berjalan berurutan sebelum satu byte pun ditulis, dan akses basis data didelegasikan ke helper agar aksinya tetap ramping dan mudah diaudit:
'use server'
import { z } from 'zod'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
const UpdatePostSchema = z.object({
postId: z.string().uuid(),
title: z.string().min(1).max(200),
})
export async function updatePost(formData: FormData) {
// 1. Authentication — who is actually calling this endpoint?
const session = await auth()
if (!session?.user) {
throw new Error('Unauthorized')
}
// 2. Server-side validation at the boundary (never trust the client)
const parsed = UpdatePostSchema.safeParse({
postId: formData.get('postId'),
title: formData.get('title'),
})
if (!parsed.success) {
throw new Error('Invalid input')
}
const { postId, title } = parsed.data
// 3. Authorization / ownership — does THIS user own THIS row? (stops IDOR)
const post = await db.post.findUnique({ where: { id: postId } })
if (!post || post.authorId !== session.user.id) {
throw new Error('Forbidden')
}
await db.post.update({ where: { id: postId }, data: { title } })
revalidatePath('/posts')
return { success: true }
}Perhatikan pemeriksaan kepemilikan pada baris post.authorId. Autentikasi saja akan dengan senang hati membiarkan pengguna login mana pun menghapus atau menyunting post mana pun berdasarkan ID. Celah itu adalah kerentanan Server Action paling umum yang saya temui saat review, dan ia tak terlihat di sistem tipe karena ID hanyalah sebuah string. Pencarian basis data plus pemeriksaan kesetaraan itulah yang menutupnya.
Jangan mengandalkan redirect di bagian atas halaman untuk melindungi aksi di dalamnya. Pemeriksaan auth tingkat halaman mengatur UI mana yang dirender. Server Action adalah titik masuk yang sepenuhnya terpisah dan harus memverifikasi ulang pemanggil sendiri. Dokumentasi menyebut pemeriksaan auth di dalam aksi sebagai kritis justru karena alasan ini, dan hal yang sama berlaku untuk pemeriksaan proxy atau middleware, yang bersifat optimistik, bukan jaminan di sumber data.
| Aspek | Aksi tidak aman | Aksi diperkuat |
|---|---|---|
| Autentikasi | Bergantung pada redirect halaman atau tombol tersembunyi | Membaca ulang sesi di dalam tubuh aksi |
| Penanganan masukan | Memercayai formData dan argumen bertipe apa adanya | Mem-parse setiap field dengan Zod safeParse dulu |
| Otorisasi | Bertindak atas ID apa pun yang diberikan pemanggil | Memuat baris dan memeriksa kepemilikan, memblokir IDOR |
| Nilai balik | Mengembalikan catatan basis data mentah | Hanya mengembalikan field yang dibutuhkan UI |
| Kontrol penyalahgunaan | Tanpa pembatasan pada write, email, atau login | Membatasi laju operasi mahal per pengguna |
Di luar tiga pemeriksaan inti, beberapa hal tambahan penting. Server Actions sudah membandingkan Origin dengan Host untuk memblokir CSRF, tetapi jika Anda berjalan di belakang reverse proxy tempat host API berbeda dari domain publik, konfigurasikan serverActions.allowedOrigins agar permintaan lintas-host yang sah tidak dibatalkan diam-diam. Untuk operasi mahal, mengirim email, menulis ke basis data, memverifikasi kredensial, tambahkan rate limiting per pengguna atau IP, karena endpoint publik tanpa pembatasan adalah undangan terbuka bagi penyalahgunaan dan credential stuffing.
Saya juga menjaga aksi tetap ramping dengan mendorong autentikasi, otorisasi, dan logika basis data ke Data Access Layer khusus-server, lalu membuat fungsi use server mendelegasikan ke sana. Ini memusatkan pemeriksaan agar konsisten dan mudah diaudit, serta menjaga rahasia dan akses process.env di satu tempat. Dua kebiasaan lagi: kembalikan hanya yang dibutuhkan UI alih-alih baris mentah, dan jangan pernah menangkap data sensitif dalam closure aksi, karena variabel yang tertutup berpindah ke klien dan kembali, hanya terlindungi enkripsi yang tidak boleh Anda andalkan sendirian.
Pemeriksaan tingkat aplikasi tidak menyelamatkan Anda dari cacat tingkat framework, dan 2025 menghadirkan yang parah. Pada 3 Desember 2025, Next.js mengumumkan CVE-2025-66478, dampak hilir App Router dari kerentanan React hulu, CVE-2025-55182, dijuluki React2Shell. Ia diberi peringkat CVSS 10.0. Protokol React Server Components mendeserialisasi masukan tak tepercaya dengan cara yang, dalam kondisi tertentu, memungkinkan permintaan yang dirancang memicu jalur eksekusi server tak diinginkan, menghasilkan remote code execution, tanpa autentikasi apa pun.
Versi yang terdampak adalah Next.js 15.x, 16.x, dan rilis canary sejak 14.3.0-canary.77 dan seterusnya, saat menggunakan React Server Components dengan App Router. Next.js 13.x, 14.x stabil, aplikasi Pages Router, dan Edge Runtime tidak terdampak. Rilis perbaikan mencakup 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, dan 16.0.7. Tidak ada solusi sementara; memperbarui wajib, dan Vercel menyarankan merotasi rahasia aplikasi setelah mem-patch dan me-redeploy.
Pelajarannya berpasangan dengan semua di atas. Auth dan validasi per-aksi Anda adalah kontrol yang Anda miliki, dan Anda harus melakukannya dengan benar, karena itulah pembeda antara aplikasi yang terkunci rapat dan API terbuka. Namun Anda juga berutang pada pengguna sebuah kebiasaan pembaruan yang membosankan dan disiplin, sehingga ketika CVSS 10.0 mendarat di hari Jumat, Anda sudah berada di lini yang ter-patch atau satu perintah dari sana. Keamanan adalah batas ditambah patch, bukan salah satu saja.