ViraStack
  • Akademi
  • Hakkında
  • Destek
Genel BakışKaynaklarKatkı
ViraStack

Frontend ekosistemi: ücretsiz rehberler, proje şablonları, AI kuralları ve form bileşenleri.

Ürünler

  • ViraStack Start
  • ViraStack AI
  • ViraStack Mask
  • ViraStack Password
  • ViraStack Guide

Keşfet

  • Akademi
  • Hakkında
  • Marka
  • Destek

© 2026 ViraStack.

Açık kaynak · MIT

Auth ve Güvenlik

09 / 09Proje

Korumalı yönetim paneli

Giriş, rol kontrolü ve güvenlik başlıklarıyla küçük bir panel kur.

Senaryo

Bir mağazanın yönetim panelini yazacaksınız. Panel:

  • E-posta ve parolayla giriş ister. Giriş yapmamış kullanıcı /sign-in'e gider.

  • Üç rol tanır: customer panele giremez, editor fiyat günceller, admin ürün arşivler ve rol değiştirir.

  • Ürün açıklamasını HTML olarak gösterir ama içindeki script'i temizler.

  • Arşivleme bir Route Handler'dır ve başka siteden gelen isteği reddeder.

  • Her sayfada nonce'lu CSP ve güvenlik başlıkları vardır.

  • Hepsini Playwright testleriyle korur.

Her parçanın hangi dersten geldiği:

ParçaDosyaDers
Better Auth, giriş, proxylib/auth.ts, app/sign-in/, proxy.tsNext.js'te auth
Rol ve izin, DAL, IDOR'a karşı kontrollib/permissions.ts, lib/dal.ts, app/admin/actions.tsYetkilendirme ve roller
Temizlenmiş HTML, CSPlib/products.ts, proxy.tsXSS ve CSP
Sec-Fetch-Site, güvenlik başlıklarılib/csrf.ts, next.config.tsCSRF, CORS ve güvenlik başlıkları
DTO, allowScriptslib/users.ts, package.jsonSecret ve bağımlılık güvenliği

Projenin çalışan hali GitHub'da: examples/auth-security/project. Uygulama ve 12 test orada; takıldığınızda karşılaştırın.

İpucu

Proje next@16.4.2, better-auth@1.7.12, better-sqlite3@13.0.3 ve isomorphic-dompurify@4.6.0 ile yazıldı. Testler Chromium'da production build üzerinde çalıştırıldı.

Adım 1: Projeyi Kurun

$ pnpm create next-app@latest admin-panel --yes
$ pnpm add better-auth better-sqlite3 isomorphic-dompurify zod server-only

create-next-app, next.config.ts'e cacheComponents: true yazar. Bu projede onu kapatın: nonce'lu CSP, Partial Prerendering ile çalışmaz.1 Yönetim paneli zaten kişiye özel olduğu için statik kabuktan bir şey kaybetmezsiniz.

// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: false,
  poweredByHeader: false,
  serverExternalPackages: ["better-sqlite3"],
  experimental: {
    authInterrupts: true,
  },
  async headers() {
    return [{ source: "/:path*", headers: securityHeaders }];
  },
};

securityHeaders dizisi CSRF, CORS ve güvenlik başlıkları dersindeki listenin aynısıdır. authInterrupts, 403 sayfası gösteren forbidden() fonksiyonunu açar; şu an deneyseldir.2

Kök layout'ta await connection() çağırın. Nonce her istekte üretildiği için hiçbir sayfa statik kalmamalı; aksi halde giriş formunun script'leri nonce'suz kalır ve CSP onları engeller.1 Testte bu satır olmadan /sign-in statik üretildi ve form çalışmadı.

İpucu

npm 12 kullanıyorsanız kurulumda better-sqlite3, esbuild ve unrs-resolver için script uyarısı görürsünüz. Örnek projede üçü de package.json'daki allowScripts alanında onaylı.

Adım 2: Kullanıcı ve Rol

Rol, kullanıcının kendisinin gönderemeyeceği bir alandır:3

// lib/auth.ts
export const auth = betterAuth({
  database: db,
  emailAndPassword: { enabled: true },
  user: {
    additionalFields: {
      role: { type: "string", defaultValue: "customer", input: false },
    },
  },
  plugins: [nextCookies()],
});

Tabloları npx auth migrate --yes açar. scripts/seed.ts üç kullanıcı ve üç ürün ekler; biri bilerek <img onerror> taşır.

Adım 3: İzinler ve Data Access Layer

İzinler Yetkilendirme ve roller dersindeki gibi tek dosyadadır. customer'ın listesi boştur:

// lib/permissions.ts
const permissions = {
  customer: [],
  editor: ["product:read", "product:update"],
  admin: ["product:read", "product:update", "product:archive", "user:manage"],
} as const satisfies Record<Role, readonly string[]>;

requirePermission artık Error fırlatmak yerine forbidden() çağırır. Next.js 403 durum kodu döner ve app/forbidden.tsx'i gösterir:2

// lib/dal.ts
export async function requirePermission(permission: Permission) {
  const user = await getCurrentUser();

  if (!hasPermission(user.role, permission)) {
    forbidden();
  }

  return user;
}

Her sayfa ilk satırda bunu çağırır: /admin için product:read, /admin/users için user:manage.

Adım 4: Ürünler ve Temiz HTML

Veri katmanı tablo satırını değil, sayfanın ihtiyacı olan alanları döndürür. Açıklama burada, üretildiği yerde temizlenir:45

// lib/products.ts
return rows.map((row) => ({
  id: row.id,
  name: row.name,
  price: row.price,
  description: { __html: DOMPurify.sanitize(row.description) },
}));
<td dangerouslySetInnerHTML={product.description} />

Testte <b>4K</b> panel<img src="x" onerror="..."> açıklaması kalın yazıyla ve onerror'sız basıldı.

Adım 5: Server Action'lar

Her action izin kontrolüyle başlar, sonra girdiyi Zod ile doğrular. Sayfadaki kontrol action'ı korumaz; action doğrudan çağrılabilir:6

// app/admin/actions.ts
export async function setRoleAction(formData: FormData) {
  const currentUser = await requirePermission("user:manage");
  const { id, role } = roleSchema.parse(Object.fromEntries(formData));

  if (id === currentUser.id) {
    throw new Error("Kendi rolünüzü değiştiremezsiniz.");
  }

  setRole(id, role);
  revalidatePath("/admin/users");
}

Son yöneticinin kendini customer yapıp paneli sahipsiz bırakmasını bu kontrol engeller.

Adım 6: Arşivleme Route Handler'ı

Arşivleme bir istemci bileşeninden fetch ile çağrılır. Route Handler önce isteğin nereden geldiğine, sonra oturuma, en son izne bakar:

// app/api/admin/products/[id]/archive/route.ts
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
  if (isCrossSiteRequest(request)) {
    return new Response(null, { status: 403 });
  }

  const session = await auth.api.getSession({ headers: await headers() });

  if (!session) {
    return new Response(null, { status: 401 });
  }

  const role = isRole(session.user.role) ? session.user.role : "customer";

  if (!hasPermission(role, "product:archive")) {
    return new Response(null, { status: 403 });
  }

  const { id } = await params;
  return archiveProduct(id) ? new Response(null, { status: 204 }) : new Response(null, { status: 404 });
}

isCrossSiteRequest, CSRF dersindeki Sec-Fetch-Site kontrolüdür.7

Adım 7: Proxy

proxy.ts iki iş yapar. /admin için cookie yoksa /sign-in'e yönlendirir; bu yalnızca iyimser bir kontroldür, asıl kontrol DAL'dadır.8 Sonra her sayfa isteğine nonce'lu CSP ekler. CSP kısmı XSS ve CSP dersindeki kodun aynısıdır.

// proxy.ts
export function proxy(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/admin") && !getSessionCookie(request)) {
    return NextResponse.redirect(new URL("/sign-in", request.url));
  }

  // ... nonce ve Content-Security-Policy
}

Adım 8: Testler

Playwright'ın önerdiği gibi her rol bir setup projesinde bir kez giriş yapar ve oturumu dosyaya kaydeder. Testler bu dosyayı storageState ile kullanır.9

Bunun bir sebebi daha var. Better Auth production'da /sign-in/email isteklerini 10 saniyede 3 istekle sınırlar.10 Testte her test kendi girişini yapınca dördüncü giriş 429 aldı. Bu sınır kaba kuvvet (brute force) saldırılarına karşı ücretsiz bir korumadır; geliştirme ortamında kapalıdır.

TestNe kanıtlar
Giriş yapmamış kullanıcı/admin → /sign-in
customer403 sayfası
editorFiyat güncellenir; arşiv isteği 403, /admin/users 403
adminÜrün arşivlenir, rol değişir
XSSonerror silinir, script çalışmaz
CSRFBaşka siteden gönderilen form 403 alır, ürün yerinde kalır
BaşlıklarNonce'lu CSP, DENY, nosniff, X-Powered-By yok
Rol enjeksiyonuKayıtta gönderilen "role": "admin" veritabanına customer olarak yazılır
Giriş sınırıArt arda yanlış parola 429 alır

CSRF testi saldırgan sayfayı page.route ile sahte bir alan adında (https://evil.example) üretir. Tarayıcı isteği gerçekten başka bir siteden gönderir.

Adım 9: Son Kontrol

npm run lint
npm run build
npm run test:e2e

Build çıktısında bütün sayfalar ƒ (dinamik) görünmeli. npm audit geliştirme araçlarındaki bir açığı raporladı; npm audit --omit=dev ise sıfır açık gösterdi. Production'a giden paketlerle geliştirme araçlarını ayırarak okuyun.

Kendin Dene

  • lib/dal.ts'ten forbidden() satırını silin. Hangi testler kırılıyor?

  • setRoleAction'daki "kendi rolünü değiştiremez" kontrolünü silin ve tek admin olarak kendinizi customer yapın. Paneli geri almanın bir yolu var mı?

  • Route Handler'daki isCrossSiteRequest satırını silip CSRF testini çalıştırın. Neden yine de 401 aldı? (İpucu: SameSite=Lax.)

  • next.config.ts'te cacheComponents: true yapın, build alın ve giriş sayfasını açın. Konsolda ne görüyorsunuz?

  • Bir audit_log tablosu ekleyin: fiyat ve rol değişikliklerinde kim, neyi, ne zaman değiştirdi.

Kaynaklar

  • 1DokümanNext.js: Content Security Policy

    Nonce için dinamik render zorunluluğu ve Partial Prerendering ile uyumsuzluk.

  • 2DokümanNext.js: forbidden

    403 sayfası, `forbidden.tsx` ve deneysel `authInterrupts` ayarı.

  • 3DokümanBetter Auth: Database — Extending core schema

    `additionalFields` ve `input: false`.

  • 4DokümanNext.js: How to think about data security

    DTO, Data Access Layer ve her Server Action'da yeniden kontrol.

  • 5DokümanOWASP: Cross Site Scripting Prevention Cheat Sheet

    `dangerouslySetInnerHTML` ve DOMPurify ile temizleme.

  • 6DokümanOWASP: Authorization Cheat Sheet

    Varsayılan olarak reddetme ve her istekte kontrol.

  • 7DokümanOWASP: Cross-Site Request Forgery Prevention Cheat Sheet

    Fetch Metadata ile CSRF kontrolü.

  • 8DokümanBetter Auth: Next.js integration

    `getSessionCookie`'nin cookie'yi doğrulamaması ve proxy'de iyimser kontrol.

  • 9DokümanPlaywright: Authentication

    Setup projesi, `storageState` ve rol başına oturum.

  • 10DokümanBetter Auth: Rate Limit

    Production'da varsayılan sınır ve `/sign-in/email` için 10 saniyede 3 istek.

Bu sayfayı düzenle

Son güncelleme tarihi: 22 Ekim 2026

Önceki adım08. Secret ve bağımlılık güvenliği