2026-03-15
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.
Özet
Broken access control, OWASP Top 10’da (2021, 2025 güncellemesinde de doğrulandı) bir numaralı güvenlik açığı. İzin sistemi hataları genellikle tek bir eksik kontrolden kaynaklanmıyor — kontrolleri unutmayı kolay, tutarlı tutmayı zor kılan bir mimariden kaynaklanıyor. Bu yazıda izin sistemlerinin neden bozulduğunu inceliyor, fail-closed prensibini tanıtıyor ve iyi tasarlanmış bir izin sisteminin karşılaması gereken hedefleri belirliyoruz.
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) sorusunu yanıtlar: “Sen kimsin?” Kimliği doğrular — geçerli oturum token’ları, JWT’ler, OAuth akışları. İkilidir. Kullanıcının geçerli bir oturumu ya vardır ya yoktur.
Yetkilendirme (authorization) sorusunu yanıtlar: “Ne yapabilirsin?” Kullanıcıya, kaynağa ve eyleme göre izinleri değerlendirir. Bağlamsaldır. Bir kullanıcı kendi belgelerini düzenleyebilir ama başkasının belgelerini düzenleyemeyebilir — tamamen kimliği doğrulanmış olmasına rağmen.
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ında403dö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 Kontrolü Tuzakları
Dağınık İzin Kontrolleri
En yaygın anti-pattern, izin mantığının sayfalara, sunucu eylemlerine ve API route’larına kopyalanması — her birinin biraz farklı mantığa sahip olmasıdı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);
}
Üç konum. Üç farklı izin kontrolü. Her birinde farklı bir hata var. “Editörler artık yayınlayabilir” gibi bir kural değişikliği, her konumun bulunup güncellenmesini gerektiriyor. “Kullanıcı X, kaynak Z üzerinde eylem Y’yi yapabilir mi?” sorusunun tek bir doğru kaynağı 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 her şeyi döndürüyor. Kullanıcının rolüne göre filtreleme yok. Viewer taslak belgeleri alıyor. Author, 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 olabilir — ya da daha kötüsü, olmaması gerektiğinde başarılı olabilir — çünkü sunucu tarafı kontrolü istemci tarafındakinden farklı 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 bir diğer tuzaktır — ve iyi belgelenmiş bir 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 — birden fazla katmanda kontroller — 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 -- herhangi bir 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 -- 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ı Olma
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üncellenebilir Olma
“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. Denetlenebilir Olma
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. Performanslı Olma
İ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-Safe Olma
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, bu çalışma zamanı hatası değil tip hatası olmalıdır.
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ünü değil.
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ı kolay kılar — 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
Bu yazıdaki anti-pattern’ler ortak bir kök nedeni paylaşıyor: yetkilendirme mantığı dağınık, çoğaltılmış ve tutarsız. “Bu kullanıcı bu kaynakta bu eylemi yapabilir mi?” sorusunun tek bir doğruluk kaynağı yok.
Çözüm 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, servis hata fırlatır
// -- sayfa değil.
İzinleri sayfalarda, eylemlerde ve route’larda kontrol etmek yerine, uygulama kodunuz ile veritabanınız arasında oturan bir service layer’da bir kez kontrol edersiniz. Artık dağınık kontroller yok. Tutarsızlıklar yok. Unutulan izin mantığı yok.
Kaynaklar
- OWASP Top 10:2025 - A01 Broken Access Control - Broken access control, 2021 ve 2025 sürümlerinde doğrulanmış bir numaralı web uygulaması güvenlik riski
- OWASP Authorization Cheat Sheet - Varsayılan olarak reddetme ve sunucu tarafı uygulama dahil yetkilendirme için kapsamlı en iyi uygulamalar
- OWASP Fail Securely - 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) - 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) - Etkilenen sürümler, exploit mekanizması ve azaltma stratejilerinin detaylı dökümü
- Next.js Authentication Guide (Resmi) - 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) - 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) - Aşırı izinler ve tutarsız mantık dahil yetkilendirme anti-pattern’leri kataloğu
- Understanding Fail Open and Fail Closed (AuthZed) - Fail-open ve fail-closed pattern’lerinin kod örnekleriyle detaylı açıklaması
- Authentication vs. Authorization (Microsoft Identity Platform) - Kimlik doğrulama ve yetkilendirme ayrımı hakkında referans dokümantasyon
- Google Zanzibar Paper - 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.
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 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'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.
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrarlanan izin mantığını ortadan kaldıran otomatik veritabanı sorgusu filtreleme ile genişletin.
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı bir kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini seçmek için karar çerçevelerini kullanın.