Kimlik Doğrulama vs Yetkilendirme: Temelleri ve İzin Sistemleri Neden Bozulur
Authentication ve authorization farkı, yaygın izin sistemi tuzakları, fail-closed prensibi ve her izin sisteminin karşılaması gereken hedefler.
Broken access control, OWASP Top 10’da (2021, 2025 güncellemesinde de doğrulandı) bir numaralı güvenlik açığı. İzin hatalarının çoğunun kökeninde, kontrolleri unutmayı kolay ve tutarlı tutmayı zor kılan bir mimari var. Kod tabanı büyüdükçe, bir kontrolün sessizce eksik kalabileceği yerlerin sayısı da büyüyor.
İlk günden benimsenmeye değer varsayılan, tüm veri erişiminin üzerinden geçtiği izin farkında tek bir katmandır; kontrol tamamlanamadığında erişimi reddeden bir katman. Oraya giden yol, pek çok kod tabanının bulanıklaştırdığı bir ayrımla başlar.
Kimlik Doğrulama ve Yetkilendirme#
Bu iki kavram genellikle tek bir “auth” kelimesi altında birleştiriliyor ve bu da karışıklığa yol açıyor. Temelde farklı konulardır.
Kimlik doğrulama (authentication) şu soruyu yanıtlar: “Sen kimsin?” Kimliği doğrular: geçerli oturum token’ları, JWT’ler, OAuth akışları. Yanıtı ikilidir; kullanıcının geçerli bir oturumu ya vardır ya yoktur.
Yetkilendirme (authorization) şu soruyu yanıtlar: “Ne yapabilirsin?” Kullanıcıya, kaynağa ve eyleme göre izinleri değerlendirir, yani bağlama bağlıdır. Bir kullanıcı, kimliği tamamen doğrulanmış olsa bile, kendi belgelerini düzenleyip başkasının belgelerine dokunamayabilir.
Bu ikisinin karıştırılması yaygın bir hata kalıbı oluşturur: kullanıcının giriş yapıp yapmadığını kontrol eden ama belirli bir eylemi gerçekleştirmeye yetkili olup olmadığını asla kontrol etmeyen kod tabanları.
HTTP durum kodları bu ayrımı yansıtır. 401 Unauthorized yanıtı, sunucunun kullanıcının kim olduğunu bilmediği anlamına gelir (kafa karıştırıcı ismine rağmen, “kimlik doğrulanmamış” demektir). 403 Forbidden yanıtı ise sunucunun kullanıcının kim olduğunu bildiği ama izni olmadığı anlamına gelir.
Tip
Uygulamanız izin hatalarında 401, giriş sorunlarında 403 döndürüyorsa, bu kimlik doğrulama ve yetkilendirmenin kod tabanında birbirine karıştırıldığının bir işaretidir.
Örnek Uygulama#
Bu seri boyunca, sürekli örneğimiz olarak bir proje ve belge yönetimi uygulaması kullanacağız. Domain tipleri şöyle:
interface User {
id: string;
email: string;
role: 'admin' | 'editor' | 'author' | 'viewer';
}
interface Project {
id: string;
name: string;
ownerId: string;
visibility: 'public' | 'private';
members: ProjectMember[];
}
interface ProjectMember {
userId: string;
role: 'editor' | 'author' | 'viewer';
}
interface Document {
id: string;
projectId: string;
title: string;
content: string;
authorId: string;
status: 'draft' | 'review' | 'published';
}
Hedeflenen izinler:
- Admin: Her şeye tam erişim
- Editor: Üyesi olduğu projelerdeki belgeleri düzenleyebilir ve yayınlayabilir
- Author: Kendi belgelerini oluşturabilir, düzenleyebilir ve incelemeye gönderebilir
- Viewer: Erişimi olan projelerdeki yayınlanmış belgeleri okuyabilir
Teoride yeterince basit. Pratikte, işlerin bozulmaya başladığı yer tam burası.
Naif İzin Kontrollerinin Tuzakları#
Dağınık İzin Kontrolleri#
En yaygın anti-pattern, izin mantığının sayfalara, sunucu eylemlerine ve API route’larına kopyalanmasıdır; her kopya zamanla diğerlerinden biraz uzaklaşır.
Sayfa bileşeni (app/projects/[id]/page.tsx):
// İzin kontrolü #1: sayfa bileşeninde
export default async function ProjectPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const session = await getSession();
const project = await db.project.findUnique({
where: { id },
});
if (project.visibility === 'private') {
const isMember = project.members.some(
(m) => m.userId === session.userId
);
if (!isMember && session.user.role !== 'admin') {
redirect('/unauthorized');
}
}
return <ProjectView project={project} />;
}
Sunucu eylemi (app/actions/documents.ts):
// İzin kontrolü #2: benzer ama ince farklarla
'use server';
export async function updateDocument(
documentId: string,
content: string
) {
const session = await getSession();
const document = await db.document.findUnique({
where: { id: documentId },
});
// Hata: proje üyeliğini kontrol etmeyi unutmuş!
// Sadece kullanıcının yazar olup olmadığını kontrol ediyor
if (document.authorId !== session.userId) {
throw new Error('Unauthorized');
}
// Hata: editörler de düzenleyebilmeli,
// ama bu kontrol onları dışlıyor
await db.document.update({
where: { id: documentId },
data: { content },
});
}
API route (app/api/projects/[id]/documents/route.ts):
// İzin kontrolü #3: yine farklı bir varyasyon
export async function GET(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const session = await getSession();
// Hata: admin VEYA üye kontrolü yapıyor ama proje
// görünürlüğünü kontrol etmiyor. Public proje belgeleri
// herkese açık olmalı.
if (session.user.role !== 'admin') {
const project = await db.project.findUnique({
where: { id },
});
const isMember = project.members.some(
(m) => m.userId === session.userId
);
if (!isMember) {
return new Response('Forbidden', { status: 403 });
}
}
const documents = await db.document.findMany({
where: { projectId: id },
});
// Hata: taslaklar dahil TÜM belgeleri viewer'lara döndürüyor
return Response.json(documents);
}
Üç konumda, birbirinden biraz farklı üç izin kontrolü var ve her birinin kendi hatası var. “Editörler artık yayınlayabilir” gibi bir kural değişikliği, her konumu tek tek bulup güncellemeyi gerektiriyor. “Kullanıcı X, kaynak Z üzerinde eylem Y’yi yapabilir mi?” sorusunu tek bir yerde yanıtlayan bir kaynak yok.
Aşırı İzin: Gereğinden Fazla Veri Döndürme#
Bir diğer yaygın tuzak, kullanıcının görmesi gerekenden fazla veri döndürmektir:
export async function GET(req: Request) {
const session = await getSession();
const documents = await db.document.findMany({
where: { projectId: req.params.projectId },
include: { author: true, reviews: true },
});
// Viewer, bilmemesi gereken taslak belgeleri görüyor
// Author, sadece editörlere yönelik inceleme geri
// bildirimlerini görüyor
return Response.json(documents);
}
Sorgu, kullanıcının rolüne göre hiçbir filtreleme yapmadan her şeyi döndürüyor. Viewer taslak belgeleri, author ise editörlere yönelik inceleme yorumlarını alıyor. API “çalışıyor” ama veri sızdırıyor.
Katmanlar Arası Tutarsız Kontroller#
Aynı “belge düzenleyebilir mi” izni, üç farklı yerde farklı şekilde kontrol ediliyor:
// UI bileşeni: rol string'ine göre kontrol ediyor
function EditButton({ user, document }: Props) {
const canEdit =
user.role === 'admin' ||
user.role === 'editor' ||
document.authorId === user.id;
// Eksik: proje üyeliğini kontrol etmiyor
if (!canEdit) return null;
return <button>Edit</button>;
}
// Sunucu eylemi: farklı şekilde kontrol ediyor
async function editDocument(docId: string, content: string) {
const session = await getSession();
const doc = await getDocument(docId);
// Farklı mantık: yazar kontrolü yapıyor ama editör rolünü unutuyor
if (
doc.authorId !== session.userId &&
session.user.role !== 'admin'
) {
throw new Error('Cannot edit');
}
// ...
}
// Middleware: başka bir şekilde kontrol ediyor
export function middleware(req: NextRequest) {
const session = getSessionFromCookie(req);
if (!session) {
return NextResponse.redirect(new URL('/login', req.url));
}
// Sadece kimlik doğrulama kontrolü, yetkilendirme yok
return NextResponse.next();
}
Sonuç: Edit butonu doğru kullanıcılara gösteriliyor ama tıklandığında başarısız olabiliyor (ya da daha kötüsü, olmaması gerektiği halde başarılı olabiliyor), çünkü sunucu tarafındaki kontrol istemci tarafındakinden farklı bir mantık kullanıyor.
Next.js’e Özgü Tuzaklar#
Layout tabanlı yetkilendirme, Next.js uygulamalarında yaygın bir hatadır:
// app/dashboard/layout.tsx
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
if (!session) {
redirect('/login');
}
// Sorun 1: Layout'lar istemci tarafı navigasyonda yeniden
// render edilmez. Oturum kontrolü bir kez çalışır ama
// sonraki route değişikliklerinde çalışmaz.
// Sorun 2: İç route'lardaki Server Action'lar bu kontrolü
// atlar. Bir server action her yerden çağrılabilir.
// Sorun 3: /dashboard altındaki API route'lar da bu
// layout tarafından korunmaz.
return <>{children}</>;
}
Sadece middleware tabanlı yetkilendirme, iyi belgelenmiş bir başka tuzaktır. CVE-2025-29927 (CVSS 9.1), Next.js’teki x-middleware-subrequest header’ının tüm middleware tabanlı erişim kontrolünü atlamak için kullanılabileceğini gösterdi. Sadece middleware’de yaşayan herhangi bir koruma, tek bir HTTP header’ı ile aşılabiliyordu.
// middleware.ts: güvenlik açığı bulunan pattern
export function middleware(request: NextRequest) {
const session = getSessionFromCookie(request);
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!session || session.role !== 'admin') {
return NextResponse.redirect(
new URL('/login', request.url)
);
}
}
return NextResponse.next();
}
// CVE-2025-29927: Saldırgan x-middleware-subrequest
// header'ı ekleyerek TÜM middleware'i atlayabiliyordu.
// Yetkilendirme, veri erişim katmanında yapılmalıdır.
Bu güvenlik açığı temel bir prensibi gösteriyor: yetkilendirme, sadece middleware veya routing seviyesinde değil, veri erişim katmanında yapılmalıdır. Derinlemesine savunma, yani birden fazla katmanda kontrol, zorunludur.
”Fail Closed” Prensibi#
Bir izin kontrolü hatayla karşılaştığında (veritabanı zaman aşımı, beklenmeyen bir istisna, eksik bir alan) iki olası varsayılan davranış vardır:
Fail open (tehlikeli): Kontrol başarısız olduğunda erişim ver.
async function checkPermission(
userId: string,
resourceId: string
): Promise<boolean> {
try {
const permission = await db.permission.findFirst({
where: { userId, resourceId },
});
return permission !== null;
} catch (error) {
console.error('Permission check failed:', error);
return true; // FAIL OPEN: hata durumunda erişim veriyor
}
}
Fail closed (güvenli): Kontrol başarısız olduğunda erişimi reddet.
async function checkPermission(
userId: string,
resourceId: string
): Promise<boolean> {
try {
const permission = await db.permission.findFirst({
where: { userId, resourceId },
});
return permission !== null;
} catch (error) {
console.error('Permission check failed:', error);
return false; // FAIL CLOSED: hata durumunda erişimi reddediyor
}
}
Bu tuzağın daha ince bir versiyonu isAdmin boolean varsayılanıdır:
// TEHLİKELİ: varsayılan değer true
let isAdmin = true;
try {
const user = await getUser(session.userId);
isAdmin = user.role === 'admin';
} catch (error) {
logger.error('Failed to fetch user role', error);
// isAdmin true kalıyor, yani her hata admin erişimi veriyor
}
// GÜVENLİ: varsayılan değer false
let isAdmin = false;
try {
const user = await getUser(session.userId);
isAdmin = user.role === 'admin';
} catch (error) {
logger.error('Failed to fetch user role', error);
// isAdmin false kalıyor, yani hata admin erişimi yok demek
}
Kural basit: isAuthorized(), isAuthenticated() ve validate() gibi güvenlik metotları herhangi bir istisna durumunda false döndürmelidir. Şüphe varsa, erişimi reddet.
Warning
Bu durum allowlist ve blocklist karşılaştırması için de geçerlidir. Allowlist (“sadece bu roller erişebilir”) fail-closed çalışır: yeni bir rolün varsayılan olarak erişimi yoktur. Blocklist (“bu rolleri engelle”) fail-open çalışır: yeni bir rol, biri onu engelleme listesine eklemeyi hatırlayana kadar erişime sahiptir.
İyi Bir İzin Sisteminin Hedefleri#
Bir çözüm oluşturmadan önce, iyi bir izin sisteminin neyi başarması gerektiğini tanımlamak faydalıdır. Bu hedefler, serinin geri kalanına rehberlik edecek:
1. Yetkisiz Erişimi Önleme#
Hem yatay yetki yükseltme (kullanıcı A’nın kullanıcı B’nin verilerine erişmesi) hem de dikey yetki yükseltme (viewer’ın admin eylemleri gerçekleştirmesi) önlenmelidir. Her endpoint ve her veri sorgusu, belirli kullanıcıyı, kaynağı ve eylemi dikkate alan bir yetkilendirme kontrolüne ihtiyaç duyar.
2. Tutarlılık#
Aynı izin mantığı her yerde geçerli olmalıdır. Bir editör UI’da belge düzenleyebiliyorsa, aynı kural sunucu eylemlerinde, API route’larında ve veritabanı sorgularında da geçerli olmalıdır. İzin kuralları için tek bir doğruluk kaynağı, katmanlar arasındaki tutarsızlığı ortadan kaldırır.
3. Varsayılan Olarak Otomatik Uygulama#
Geliştiricilerin izin kontrolü eklemeyi hatırlaması gerekmemelidir. Sistem mimarisi, yetkisiz erişimi varsayılan olarak imkansız kılmalıdır. Verilere erişmek izin-farkında bir service layer üzerinden geçmeyi gerektiriyorsa, eksik bir kontrol güvenlik açığı yerine derleme hatası olur.
4. Kolay Güncellenebilirlik#
“Editörler artık yayınlayabilir” gibi bir izin kuralı değişikliği, tek bir yerde tek bir değişiklik gerektirmelidir. Bir izin kuralını güncellemek için tüm kod tabanında dağınık kontrolleri aramak gerekiyorsa, hatalar kaçınılmazdır.
5. Denetlenebilirlik#
Kimin neye, ne zaman eriştiğini bilmek gerekir. Fintech, sağlık ve diğer düzenlenmiş sektörlerdeki uyumluluk gereksinimleri, erişim denetim izleri talep eder. Düzenlenmiş sektörlerin dışında bile, denetim günlükleri izin sorunlarını hata ayıklamak için çok değerlidir.
6. Performans#
İzin kontrolleri her istekte, genellikle istek başına birden fazla kez gerçekleşir. Önemli gecikme ekleyemezler. Önbellekleme stratejileri, verimli veri yapıları ve minimum veritabanı sorguları hep dikkate alınması gereken konulardır.
7. Type Safety#
TypeScript’te, tip sistemi izin hatalarını derleme zamanında yakalamalıdır. Bir rol yeniden adlandırılırsa, derleyici her referansı işaretlemelidir. Bir izin kontrolü kaynak nesnesi gerektiriyorsa ama undefined alıyorsa, derleyici bu kodu çalışmadan önce reddetmelidir.
Gerçek Dünya Senaryoları: Pratikte İzin Hataları#
Anti-pattern’lerin nasıl ortaya çıktığını görmek için örnek uygulamadaki belirli hata senaryolarını inceleyelim.
Senaryo 1: Yatay Yetki Yükseltme#
Kullanıcı A, Proje X’te bir yazar. URL’deki belge ID’sini /projects/x/documents/123’ten /projects/y/documents/456’ya değiştiriyor. Sayfa bileşeni “kullanıcı kimliği doğrulanmış mı?” diye kontrol ediyor ama “bu kullanıcının Proje Y’ye erişimi var mı?” diye asla kontrol etmiyor.
Kullanıcı A artık hiçbir ilişkisi olmayan bir projeden belgeleri okuyor.
Senaryo 2: Dikey Yetki Yükseltme#
Bir viewer, “belge yayınla” sunucu eyleminin tahmin edilebilir bir endpoint’te bulunduğunu keşfeder. UI, viewer’lar için “Yayınla” butonunu gizler ama sunucu eylemi yalnızca kullanıcının kimliğinin doğrulanıp doğrulanmadığını kontrol eder, rolüne hiç bakmaz.
Viewer, sunucu eylemini doğrudan çağırarak belgeleri yayınlar.
Senaryo 3: IDOR (Güvensiz Doğrudan Nesne Referansı)#
API endpoint’i /api/documents/123, belge veritabanında varsa döndürür. İstekte bulunan kullanıcının belgeyle veya projesiyle herhangi bir ilişkisi olup olmadığına dair kontrol yoktur. Ardışık ID’ler numaralandırmayı kolaylaştırır: saldırgan ID’ler üzerinde ilerleyerek sistemdeki tüm belgeleri indirir.
Senaryo 4: Tutarsız Rol Uygulaması#
Admin paneli, UI üzerinden yalnızca adminlere erişilebilirdir (istemci tarafı kontrolü navigasyon bağlantısını gizler). Ancak admin API route’ları (/api/admin/users, /api/admin/settings) rol doğrulamasından yoksundur. URL’yi bilen herhangi bir kimliği doğrulanmış kullanıcı, admin API’lerini doğrudan çağırabilir.
Bu senaryoların her biri ortak bir kök nedeni paylaşır: dağınık, çoğaltılmış veya tamamen eksik yetkilendirme mantığı.
Yaygın Tuzaklar Özeti#
| Yaklaşım | Avantaj | Problem |
|---|---|---|
| Dağınık ad-hoc kontroller | Başlangıçta hızlı uygulanabilir | Tutarsız, bakımı zor, güvenlik açıkları oluşturur |
| Sadece middleware kontrolleri | Her istekte çalışır | Atlanabilir (CVE-2025-29927), kaynak düzeyinde bağlam yok |
| Layout tabanlı kontroller | Merkezi hissedilir | Navigasyonda yeniden render edilmez, sunucu eylemlerini korumaz |
| Sadece istemci tarafı kontroller | İyi UX (erişilemeyen UI’ı gizler) | Sıfır güvenlik değeri; sunucu her zaman doğrulamalı |
Sıradaki#
Yukarıdaki dağınık kontrollerden çıkış yolu merkezileştirmedir. Sonraki yazıda, service layer pattern’ini tanıtacağız: tüm veri erişiminin geçtiği, yetkilendirmenin bir kez ve doğru şekilde kontrol edildiği tek bir katman.
// Ön izleme: tam uygulama Post 102'de
// Her yerde dağınık kontroller yerine,
// tüm erişim izin-farkında bir servis üzerinden geçer:
const document = await documentService.getById(
session.userId,
documentId
);
// Servis şunları yönetir: kimlik doğrulama, yetkilendirme
// ve veri filtreleme.
// Kullanıcı bu belgeye erişemezse, sayfa render edilmeden
// servis hata fırlatır.
Service layer, uygulama kodu ile veritabanı arasında gerçek bir sınır olduğu sürece varsayılan olarak işini görür; sayfa, sunucu eylemi ve API route üç ayrı kopya yerine tek bir kuralı paylaşır. Biri veritabanını doğrudan sorguladığı anda bu güvence kayboluyor, dolayısıyla sınırı belgelemek yetmez, kod düzeyinde zorunlu kılmak gerekir.
Kaynaklar#
- OWASP Top 10:2025 - A01 Broken Access Control (yeni sekmede açılır) - Broken access control, 2021 ve 2025 sürümlerinde doğrulanmış bir numaralı web uygulaması güvenlik riski
- OWASP Authorization Cheat Sheet (yeni sekmede açılır) - Varsayılan olarak reddetme ve sunucu tarafı uygulama dahil yetkilendirme için kapsamlı en iyi uygulamalar
- OWASP Fail Securely (yeni sekmede açılır) - Güvenlik metotlarının istisna durumunda neden false döndürmesi gerektiği ve isAdmin varsayılan tuzağı örneği
- CVE-2025-29927: Next.js Middleware Authorization Bypass (ProjectDiscovery) (yeni sekmede açılır) - x-middleware-subrequest header’ı ile middleware atlamaya izin veren CVSS 9.1 güvenlik açığının teknik analizi
- CVE-2025-29927: Next.js Authorization Bypass (JFrog) (yeni sekmede açılır) - Etkilenen sürümler, exploit mekanizması ve azaltma stratejilerinin detaylı dökümü
- Next.js Authentication Guide (Resmi) (yeni sekmede açılır) - Data Access Layer pattern’i, middleware kısıtlamaları ve sunucu eylemi güvenliğini kapsayan resmi dokümantasyon
- Authorization Bugs Are Having Their SQL Injection Moment (ZeroPath) (yeni sekmede açılır) - Kritik güvenlik açıklarının %53’ünün erişim kontrolü hataları olduğunu gösteren analiz
- How to Avoid Common Authorization Errors (Cerbos) (yeni sekmede açılır) - Aşırı izinler ve tutarsız mantık dahil yetkilendirme anti-pattern’leri kataloğu
- Understanding Fail Open and Fail Closed (AuthZed) (yeni sekmede açılır) - Fail-open ve fail-closed pattern’lerinin kod örnekleriyle detaylı açıklaması
- Authentication vs. Authorization (Microsoft Identity Platform) (yeni sekmede açılır) - Kimlik doğrulama ve yetkilendirme ayrımı hakkında referans dokümantasyon
- Google Zanzibar Paper (yeni sekmede açılır) - Merkezi izin mimarileri için bağlam sağlayan Google’ın küresel yetkilendirme sistemi tasarımı
Ölçeklenebilir İzin Sistemleri
TypeScript ve Next.js ile ölçeklenebilir izin sistemleri oluşturma rehberi. Basit kontrol mekanizmalarından RBAC ve ABAC'a, oradan multi-tenant yetkilendirme sistemlerine kadar kapsamlı bir seri.
Bu serideki tüm yazılar
İlgili yazılar
Dağınık izin kontrollerini merkezi bir service layer'a taşıyın, Next.js middleware guard'ları ekleyin ve derinlemesine savunma yetkilendirme mimarisi oluşturun.
typescript · nextjs · authorization +2
TypeScript ile type-safe bir RBAC sistemi oluşturun, birleşik bir can() fonksiyonu yazın, UI ve backend'de izinleri senkronize edin ve RBAC'ın sınırlarını anlayın.
typescript · nextjs · authorization +2
TypeScript'te builder pattern, koşullu izinler ve RBAC'ın sınırlarını aşan type-safe policy değerlendirmesi ile bir ABAC policy engine oluşturun.
typescript · nextjs · authorization +2
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrar eden izin mantığını ortadan kaldıran otomatik sorgu filtrelemeyle genişletin.
typescript · nextjs · authorization +2
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini karar çerçeveleriyle seçin.
typescript · nextjs · authorization +3