Güvenli Next.js Uygulama Yapısı: Küçük Ekipler İçin Pratik Kontrol Listesi

Next.js projelerinde rota, environment değişkenleri, form güvenliği ve SEO temellerini aynı anda sağlamlaştırmak için uygulanabilir bir rehber.

10 dk okuma
ibrahimsql
1.842 kelime

Güvenli Next.js Uygulama Yapısı#

Next.js hızlı ürün çıkarmak için güçlü bir çatı sunar, fakat hız güvenlik kararlarını ertelemek için bahane olmamalıdır. Küçük bir ekip bile birkaç temel kuralı baştan koyarsa daha az kırılgan, daha okunabilir ve SEO açısından daha sağlıklı bir uygulama geliştirir.

Bu rehberin odak noktası tek bir çerçeve sürümü değildir; App Router'ın 13.4 ile kararlı hale gelmesinden bu yana geçen süreçte getirilen pratik kuralları ve güvenliği birleştiren kararlar.

Threat Model: İstemci Neyi Görmeli, Sunucu Neyi?#

Modern Next.js uygulaması ve sınırları:

[Tarayıcı] | | (1) HTML / RSC payload / client JS v [Next.js edge veya node server] | | (2) server action / route handler v [DB / SMTP / üçüncü taraf API]

Üç sınır; üç farklı izin seviyesi:

  1. Tarayıcıya gönderilen her şey public'tir. Bundle'a giren her string, kırmızı team görebilir. Bu yüzden NEXT_PUBLIC_ prefix'li değişkenler sır saklamaz.
  2. Server action ve route handler'lar public HTTP endpoint'tir. POST kabul eden her fonksiyon yetkilendirme kontrolü taşımalıdır.
  3. DB veya üçüncü taraf API sunucu tarafıdır. Sadece server component, route handler veya server action bunlara dokunmalı.

"Butonu gizlemek koruma değildir" kuralı burada da geçerlidir: istemcide koşullu render edilen buton, ağ panelinde hala endpoint'e istek gönderebilir; endpoint yetkilendirmeyi kendisi yapmalıdır.

Trust Boundaries: Güven Sınırlarını Çiz#

İstek akışı ve kontrol noktaları:

Browser fetch('/api/contact', {method: 'POST'}) | v middleware.ts ---- token var mı? (sadece presence check; asıl authz değil) | v route.ts ------ validate(input) -> authorize(user) -> rate limit | v db.insert(...) / smtp.send(...) / thirdParty.fetch(...) | v Response (aynı hata mesajı, sıfır stack trace)

Middleware'te authz yapma; orada sadece varlığını kontrol et. Asıl yetki kararı handler içinde, kullanıcı bağlamı yüklenip resource owner ile karşılaştırıldıktan sonra verilir.

Ortam Değişkenlerini İkiye Ayırın#

NEXT_PUBLIC_ ile başlayan değişkenler tarayıcıya gider. Bu yüzden API anahtarı, SMTP şifresi, veritabanı parolası veya özel token asla bu prefix ile tutulmamalıdır.

Pratik ayrım:

TürÖrnekNerede kullanılmalı?
PublicNEXT_PUBLIC_SITE_URLTarayıcı ve metadata
PrivateSMTP_PASSRoute handler veya server action
SecretDATABASE_URLSunucu tarafı veri erişimi

Bu ayrımı kod inceleme kuralı haline getirmek, ileride oluşacak veri sızıntılarını azaltır.

Tipik bir örnek: yanlışlıkla NEXT_PUBLIC_STRIPE_SECRET tanımlanırsa, next build çıktısında secret görünmeyebilir ama değişken isimlendirme hatası yoğun bir arama ile yakalanabilir:

grep -rn "NEXT_PUBLIC_" --include="*.ts" --include="*.tsx" -l | xargs grep -H "SECRET\|PASSWORD\|PRIVATE\|TOKEN\|KEY"

Yakalanan her satır, canlı ortamda sızdırılmasının önüne geçmiş bir potansiyel olaydır.

Bir de .env dosyası prensibi: .env.local git'e girmesin; repo'da .env.example tek olsun ve değişken isimlerini içerip değerleri içermesin. CI'da dotenv yerine platformun secret yönetimine güven: Vercel'de Environment Variables, kendi sunucunda systemd EnvironmentFile= veya Docker --env-file.

Route Handler'ları Doğrulama Katmanı Olun#

Bir formdan gelen veriyi doğrudan e-posta, veritabanı veya üçüncü taraf API'ye göndermek risklidir. Route handler içinde en az şu kontroller yapılmalıdır:

  1. Zorunlu alanlar var mı?
  2. E-posta formatı geçerli mi?
  3. Metin uzunluğu makul mü?
  4. Kullanıcı çok sık istek atıyor mu?
  5. Hata mesajı hassas bilgi sızdırıyor mu?

Bu kontroller basit görünür ama gerçek projelerde en çok atlanan noktalardır.

Minimal bir route handler iskeleti:

import { z } from 'zod' const schema = z.object({ email: z.string().email().max(254), message: z.string().min(10).max(2000), }) export async function POST(req: Request) { const body = await req.json().catch(() => null) const parsed = schema.safeParse(body) if (!parsed.success) { return Response.json({ error: 'invalid_input' }, { status: 400 }) } // rate limit kontrolü burada // iş mantığı burada return Response.json({ ok: true }) }

Doğrulama hatasını istemciye detaylı döndürmek yerine tek bir invalid_input çevirmek, ne test edildiğini sızdırmaz. Aynı tek mesaj yaklaşımı 404/403 ayrımında da geçerli: "bu kaynak yok" ve "bu sana açık değil" farklı mesajlarla dönülürse endpoint keşfi kolaylaşır.

Rate Limit#

Basit yaklaşım: IP başına sliding window sayacı. Upstash Redis veya yerel bellek Map'i; üretimde Redis tercih. Yanıt başlığı olarak standart RateLimit-Limit, RateLimit-Remaining ve Retry-After kullan; cron veya queue ile ayrı süreçte sıfırla.

POST /api/contact X-RateLimit-Limit: 5 X-RateLimit-Remaining: 0 Retry-After: 60 HTTP 429

Server Actions: Yetki Katmanı Doğrudan Aksiyon İçinde#

Server Actions, form action veya event handler olarak çağrılabilen fonksiyonlardır. Her server action, tarayıcıdan doğrudan POST ile çağrılabilen bir endpoint'tir. Bu yüzden:

  • Her action ilk satırında kullanıcı oturumunu doğrula.
  • Resource erişiminde owner kontrolü yap.
  • Girdiyi zod veya benzeri şemalarla doğrula.
  • Yanıtta sadece gerekli alanları döndür.
'use server' export async function updateProfile(input: unknown) { const user = await getSessionUser() if (!user) throw new Error('unauthorized') const parsed = profileSchema.parse(input) await db.update(user.id, parsed) }

"use client" tarafında serverdan gelen veriyi render etmek yerine server component'te getir; bu client'ta veri sızdırma riskini azaltır.

Metadata ve Canonical URL'leri Merkezi Yönetin#

SEO tarafında en sık görülen sorun, aynı içeriğin farklı URL'lerde tekrar etmesidir. Çok dilli sitelerde bu daha hızlı büyür. /en ve /tr gibi net dil kökleri kullanıyorsanız her sayfada canonical ve hreflang ilişkisini açıkça üretin.

İyi bir sayfa şunları taşımalıdır:

  • Dil bazlı canonical URL
  • Alternatif dil bağlantıları
  • Açıklayıcı title ve description
  • Open Graph görseli
  • Article, Breadcrumb veya WebSite JSON-LD şeması

Örnek metadata üretimi:

export async function generateMetadata({ params }): Promise<Metadata> { const { lang } = await params return { alternates: { canonical: `/${lang}/posts/example`, languages: { en: '/en/posts/example', tr: '/tr/posts/example' }, }, openGraph: { images: ['/og/example.png'] }, } }

Bileşenleri Davranışa Göre Ayırın#

Server component varsayılan olsun. Sadece state, event handler, tema veya tarayıcı API'si gereken yerlerde client component kullanın. Bu yaklaşım hem bundle boyutunu azaltır hem de veri erişimini daha güvenli tutar.

Örnek karar:

BileşenTercih
Blog listelemeServer component
Tema değiştirmeClient component
Arama input'uClient component
Metadata üretimiServer tarafı

Server component'te 'use client' alt ağacı import ettiğinde, o ağacın altına veri geçişi serialization ile yapılır. Bu nedenle serverdan client'a prop olarak sadece gereken alanı (id, title, isPublic gibi) gönder; ham DB kaydını olduğu gibi indirme.

Detection ve Mitigation Workflow#

Güvenlik açığını tespit etmenin yolları:

SemptomOlası sebep
.env.local git history'desecret sızıntısı, rotate et
Network panelinde 500 ile stack tracehandler'da try/catch + generic message yok
Bütün sayfalarda X-Powered-By: Next.jspoweredByHeader: false ayarla
Duplicate canonical URLgenerateMetadata'da canonical eksik
Tüm sayfa 'use client'component sınırı yanlış çizilmiş
DB'de ham girdiserver action'da doğrulama yok

Her satır, bir kod gözden geçirme checklist'idir. Bu listeyi PR şablonuna kopyala; her PR bir maddeye dokunmalı.

Mitigation workflow:

  1. Bulguyu staging'de yeniden üret.
  2. Düzeltmeyi en küçük yüzeyli şekilde uygula.
  3. next build && next start ile production mode test et; dev server farklı davranır.
  4. Eski davranışı test eden bir regresyon testi yaz.
  5. Değişikliği commit hash'iyle raporla.

Version Differences: Next 13 -> 14 -> 15#

  • Next 13.4+: App Router kararlı, app/ dizini ve server components standart.
  • Next 14: Server Actions kararlı (stable) oldu; form action'ları artık native. Bundled metadata API aynı, ancak streaming SSR daha olgun.
  • Next 15: params ve searchParams artık async (Promise) döndürüyor; eski sync kullanım deprecates. Cache davranışı daha muhafazakar: GET route handler'lar varsayılan olarak cache'lenmiyor.
  • Middleware: Edge runtime'da çalışır; Node API'lerine erişim sınırlı. Parolaları karşılaştırma ve DB sorgusu gibi ağır işler middleware'e taşınmamalı.
  • next.config.js: poweredByHeader: false, reactStrictMode: true, images.remotePatterns allowlist gibi küçük ayarlar savunma yüzeyini daraltır.

Bu farklılıkları package.json içindeki next alanına bakarak sürüm kontrol listesi değil, davranış farkı notu olarak tut. Bir güncellemeden sonra route handler cache davranışını tekrar doğrula.

// next.config.js module.exports = { poweredByHeader: false, reactStrictMode: true, images: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] }, }

ASCII: Güvenlik Kontrol Noktaları#

Request | v [1] middleware: oturum cookie'si var mı? | yoksa: 302 /login v [2] route handler: zod ile doğrula | başarısız: 400 {error: 'invalid_input'} v [3] authz: bu resource'un sahibi current user mı? | değilse: 404 (varlığı sızdırma) v [4] rate limit: IP/kullanıcı başına kvota | aşıldıysa: 429 + Retry-After v [5] iş mantığı + DB / SMTP / API v Response: yalnızca gerekli alanlar, genel hata mesajı

Her kutu bir satır kod satırıdır; beş katman bir sayfalık middleware'e sıkıştırılmamalı, her biri açık bir dosya ve test olmalıdır.

Sonuç#

Güvenli Next.js yapısı büyük bir framework değişikliği gerektirmez. Doğru env ayrımı, doğrulanan route handler'lar, dil bazlı metadata ve server component önceliği, küçük ekiplerde bile ciddi kalite artışı sağlar. Asıl fark, bu kuralları "hatırlanan şeyler" yerine "PR şablonundaki maddeler" haline getirmektedir.

Güvenlik Header'ları#

next.config.js üzerinden eklenmesi gerekenler:

module.exports = { poweredByHeader: false, reactStrictMode: true, async headers() { return [{ source: '/:path*', headers: [ { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-Frame-Options', value: 'DENY' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=()' }, { key: 'Content-Security-Policy', value: "default-src 'self'" }, ], }] }, }

CSP politikasını başlangıçta sıkı tut, ihtiyaç çıktıkça gevşet. unsafe-inline kullanmak CSP'nin anlamını boşaltır; Next 14 ile nonce desteği geldi, production'da nonce kullanımı tercih edilir.

Authentication Yaklaşımı#

NextAuth/Auth.js, oturum ve callback akışlarını yönetir. JWT session cookie httpOnly, secure, sameSite=lax ile tutulmalı. Secret olarak NEXTAUTH_SECRET'i üret ve rotate planı yaz. Oturum tarafında en kritik nokta: refresh token veya session token'i localStorage yerine httpOnly cookie'de tutmak. localStorage'daki token, XSS ile çalınabilir.

Form Güvenliği: CSRF ve SameSite#

Server Actions için Next, Origin başlığını otomatik kontrol eder; yine de POST endpoint'lerinde Origin kontrolünü açık tut. CSRF token alternatifi olarak SameSite=Lax yeterli olabilir, ancak ödeme veya profil değiştirme gibi kritik işlemlerde ek doğrulama (re-auth) ekle.

Logging ve Hata Yönetimi#

Production'da asla console.log(user) veya ham hata stack trace'i istemciye geçmesin. Merkezi bir log'layıcıda (src/lib/logger.ts) ve sunucu tarafında kullan; IP, user id, endpoint ve status kodu yeterli metaveridir. Client'a tek satır: { error: 'internal_error' }. Log'un kendisi de PII içermesin.

Deploy ve Secret Rotation#

AdımAçıklama
Build sırasındaSecret'lar env olarak enjekte edilir
RolloutYeni secret kademeli yayılır
VerifyHealth check ile doğrula
RotateEski secret'i revoke et
AuditKim ne zaman hangi secret'a erişti

Bu döngü, canlı bir sistemde secret değiştirmeyi tek seferlik bir panik olmaktan çıkarır.

CI/CD Tarafı#

next build çalışırken secret'lara ihtiyaç duymayan bir CI pipeline kur. Env değişkenleri NEXT_PUBLIC_ prefix'liyse build çıktısına gömülür; bu nedenle build argümanlarında secret geçirme. CI'da grep -R "NEXT_PUBLIC_.*SECRET" tarzı bir kaçak avı scripti koşturmak, deploy öncesi ucuz bir güvencedir.

Örnek Kontrol Listesi (PR Şablonuna Kopyala)#

  • Yeni env değişkenleri .env.example'a eklendi, değer yok.
  • NEXT_PUBLIC_ prefix'i olmayan hiçbir secret tarayıcıya gönderilmiyor.
  • Route handler/action girdiyi zod ile doğruluyor.
  • Authz kontrolü action içinde, ilk satırda.
  • Rate limit uygulandı.
  • Hata mesajı generic, stack trace client'a gitmiyor.
  • Canonical/hreflang metadata sayfa bazında üretildi.
  • Header'lar next.config.js'te tanımlı.
  • console.log kalmadı, logger kullanıldı.

Bu listeyi PR şablonuna yapıştırmak, güvenlik kararlarını "hatırlanan şeyler" olmaktan çıkarır.

---
Bu yazıyı paylaş:
TwitterLinkedInFacebook

Ne düşünüyorsun?

Tepki bırakarak geri bildirim ver

İlgili Yazılar