Multi-Tenant Laravel Projelerinde CSRF 419 Kısır Döngüsü Çözümü
14 Şubat 2026 • Muzibu.com
Multi-tenant yapıda çoklu session cookie çakışması:
laravel_session_1001tenant_1001_sessionEski cookie'leri tespit et → Her response'ta expire et → 419 olursa cleanup endpoint'e yönlendir
app/Http/Middleware/CleanupLegacyCookies.php
app/Http/Middleware/SetTenantSessionConfig.php
app/Http/Middleware/VerifyCsrfToken.php
app/Http/Controllers/SessionCleanupController.php
bootstrap/app.php
resources/views/errors/419.blade.php
resources/views/themes/[TEMA]/auth/login.blade.php
routes/web.php
(route ekle)
Konum: app/Http/Middleware/CleanupLegacyCookies.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Cookie;
/**
* Legacy Cookie Cleanup Middleware
*
* Her response'ta eski/yanlış formattaki session cookie'lerini temizler.
* Bu, 419 kısır döngüsünü önler.
*/
class CleanupLegacyCookies
{
/**
* Temizlenecek eski cookie adları
* Kendi tenant yapınıza göre düzenleyin!
*/
protected array $legacyCookies = [
'laravel_session_1001', // Eski tenant format
'laravel_session_1',
'laravel_session_2',
'laravel_session', // Generic Laravel
'muzibu_session', // Çok eski format (varsa)
'PHPSESSID', // PHP native
];
public function handle(Request $request, Closure $next): Response
{
// Önce response'u al
$response = $next($request);
// Request'teki cookie'leri kontrol et
$allCookies = $request->cookies->all();
$legacyInCookies = array_intersect(array_keys($allCookies), $this->legacyCookies);
// SetTenantSessionConfig'den gelen bilgiyi de ekle
$legacyFromAttribute = $request->attributes->get('_legacy_cookies_to_expire', []);
$legacyInRequest = array_unique(array_merge($legacyFromAttribute, $legacyInCookies));
// Aktif session cookie adını al
$activeSessionCookie = config('session.cookie', 'tenant_1001_session');
// Legacy cookie bulunduysa, response'a expire header'ları ekle
foreach ($legacyInRequest as $cookieName) {
// Aktif cookie'yi silme!
if ($cookieName === $activeSessionCookie) {
continue;
}
// Her domain varyasyonunda expire et - Symfony Cookie ile kesin kontrol
foreach ($this->getCookieDomains() as $domain) {
$response->headers->setCookie(
new Cookie(
$cookieName, // name
'', // value (boş)
1, // expire (1 = geçmiş timestamp = sil)
'/', // path
$domain, // domain
true, // secure
true, // httpOnly
false, // raw
'lax' // sameSite
)
);
}
}
return $response;
}
/**
* Cookie domain varyasyonlarını döndür
* Kendi domain yapınıza göre düzenleyin!
*/
protected function getCookieDomains(): array
{
$domains = [];
// Mevcut tenant'ın domain'lerini al (multi-tenant için)
if (function_exists('tenant') && tenant()) {
try {
$tenantDomains = tenant()->domains()->pluck('domain')->toArray();
foreach ($tenantDomains as $domain) {
$cleanDomain = preg_replace('/^www\./', '', $domain);
$domains[] = '.' . $cleanDomain; // .example.com
$domains[] = $cleanDomain; // example.com
$domains[] = 'www.' . $cleanDomain; // www.example.com
}
} catch (\Exception $e) {
// Hata olursa fallback
}
}
// Fallback: Request host'undan al
if (empty($domains)) {
$host = request()->getHost();
$cleanHost = preg_replace('/^www\./', '', $host);
$domains = [
'.' . $cleanHost,
$cleanHost,
'www.' . $cleanHost,
];
}
return array_unique(array_filter($domains));
}
}
Konum: app/Http/Middleware/SetTenantSessionConfig.php
Değişiklik: Legacy cookie'leri request attribute olarak sakla
Session config ayarlandıktan sonra legacy cookie tespiti:
// Session config güncellendikten SONRA ekle:
// CRITICAL: Legacy cookie'leri request'ten kaldır VE expire et
$legacyCookies = [
'laravel_session_1001',
'laravel_session',
'muzibu_session',
'laravel_session_1',
'laravel_session_2',
];
$legacyFound = [];
foreach ($legacyCookies as $legacyCookie) {
if ($legacyCookie !== $newCookieName && $request->cookies->has($legacyCookie)) {
$legacyFound[] = $legacyCookie;
// Request'ten kaldır
$request->cookies->remove($legacyCookie);
}
}
if (!empty($legacyFound)) {
// Legacy cookie'leri request attribute olarak sakla
// CleanupLegacyCookies middleware bu bilgiyi kullanacak
$request->attributes->set('_legacy_cookies_to_expire', $legacyFound);
\Log::warning('Found legacy cookies in request', [
'legacy' => $legacyFound,
'active_cookie' => $newCookieName,
]);
}
Konum: app/Http/Middleware/VerifyCsrfToken.php
Değişiklik: 419 hatası olduğunda cookie flush + session-cleanup exclude
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
protected $except = [
'api/*',
'payment/callback/*',
'telescope/telescope-api/*',
'logout',
// Session Cleanup - 419 kısır döngüsünü kırmak için
'session-cleanup',
];
/**
* Handle CSRF token mismatch with cookie flush
*/
public function handle($request, \Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return tap($next($request), function ($response) use ($request) {
if ($this->shouldAddXsrfTokenCookie()) {
$this->addCookieToResponse($request, $response);
}
});
}
// TOKEN MISMATCH - Cookie flush ile response döndür
\Log::info('CSRF Mismatch - Cookie Flush triggered', [
'ip' => $request->ip(),
'url' => $request->fullUrl(),
]);
$response = response()->view('errors.419', [], 419);
// Tüm olası session cookie'lerini expire et
// KENDİ DOMAIN'İNİZE GÖRE DÜZENLEYİN!
$cookiesToExpire = [
'tenant_1001_session',
'laravel_session_1001',
'laravel_session',
'muzibu_session',
'XSRF-TOKEN',
];
$domains = ['.example.com', 'www.example.com', 'example.com'];
foreach ($cookiesToExpire as $cookieName) {
foreach ($domains as $domain) {
$response->headers->setCookie(
\Illuminate\Support\Facades\Cookie::forget($cookieName, '/', $domain)
);
}
}
return $response;
}
}
Konum: app/Http/Controllers/SessionCleanupController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Log;
/**
* Session Cleanup Controller
*
* 419 CSRF token mismatch durumunda çağrılır.
* Tüm eski/çakışan session cookie'lerini temizler.
*/
class SessionCleanupController extends Controller
{
public function cleanup(Request $request)
{
Log::info('Session Cleanup triggered', [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Temizlenecek tüm cookie adları
// KENDİ YAPISINA GÖRE DÜZENLEYİN!
$cookiesToExpire = [
'tenant_1001_session',
'laravel_session_1001',
'laravel_session',
'muzibu_session',
'XSRF-TOKEN',
'tenant_1_session',
'tenant_2_session',
'laravel_session_1',
'laravel_session_2',
'PHPSESSID',
];
// Tüm domain varyasyonları
// KENDİ DOMAIN'İNİZE GÖRE DÜZENLEYİN!
$domains = [
'.example.com',
'www.example.com',
'example.com',
null,
];
$response = redirect('/login?cleaned=1')
->with('status', 'Oturumunuz yenilendi. Lütfen tekrar giriş yapın.');
foreach ($cookiesToExpire as $cookieName) {
foreach ($domains as $domain) {
if ($domain) {
$response->withCookie(Cookie::forget($cookieName, '/', $domain));
} else {
$response->withCookie(Cookie::forget($cookieName));
}
}
}
return $response;
}
}
Değişiklik: Middleware kayıtları + Exception handler
->withMiddleware(function (Middleware $middleware) {
// 1. CSRF Token Override (Laravel 12 için)
$middleware->web(replace: [
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class
=> \App\Http\Middleware\VerifyCsrfToken::class,
]);
// 2. Session Config - StartSession'dan ÖNCE (GLOBAL)
$middleware->prepend(\App\Http\Middleware\SetTenantSessionConfig::class);
// 3. Legacy Cookie Cleanup - Her response'ta (GLOBAL)
$middleware->append(\App\Http\Middleware\CleanupLegacyCookies::class);
// ... diğer middleware'ler
})
->withExceptions(function (Exceptions $exceptions) {
// Cookie flush helper
$flushSessionCookies = function ($response, $request) {
$cookiesToExpire = [
'tenant_1001_session',
'laravel_session_1001',
'laravel_session',
'muzibu_session',
'XSRF-TOKEN',
];
// KENDİ DOMAIN'İNİZE GÖRE DÜZENLEYİN!
$domains = ['.example.com', 'www.example.com', 'example.com'];
foreach ($cookiesToExpire as $cookieName) {
foreach ($domains as $domain) {
$response->headers->setCookie(
cookie()->forget($cookieName, '/', $domain)
);
}
}
return $response;
};
// TokenMismatchException handler
$exceptions->renderable(function (\Illuminate\Session\TokenMismatchException $e, $request) use ($flushSessionCookies) {
$response = response()->view('errors.419', [], 419);
return $flushSessionCookies($response, $request);
});
// HttpException 419 handler
$exceptions->renderable(function (\Symfony\Component\HttpKernel\Exception\HttpException $e, $request) use ($flushSessionCookies) {
if ($e->getStatusCode() == 419) {
$response = response()->view('errors.419', [], 419);
return $flushSessionCookies($response, $request);
}
});
})
Konum: resources/views/errors/419.blade.php
Değişiklik: Auto-redirect to /session-cleanup
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oturum Süresi Doldu - 419</title>
<!-- AUTO CLEANUP: 3 saniye sonra yönlendir -->
<meta http-equiv="refresh" content="3;url=/session-cleanup">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #e2e8f0;
padding: 1rem;
}
.container { max-width: 500px; text-align: center; }
.icon {
width: 80px; height: 80px;
margin: 0 auto 1.5rem;
background: linear-gradient(135deg, #f59e0b, #d97706);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 2rem;
}
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.desc { color: #94a3b8; margin-bottom: 1.5rem; }
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
text-decoration: none;
border-radius: 0.5rem;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">⏱️</div>
<h1>Oturum Süresi Doldu</h1>
<p class="desc">
<span id="countdown">3</span> saniye içinde otomatik yenilenecek...
</p>
<a href="/session-cleanup" class="btn">Hemen Yenile</a>
</div>
<script>
var s = 3;
setInterval(function() {
s--;
document.getElementById('countdown').textContent = s;
}, 1000);
</script>
</body>
</html>
Değişiklik: Form submit'te 419 yakala ve cleanup'a yönlendir
<!-- Login form'una bu attribute'ları ekle: -->
<form method="POST" action="{{ route('login') }}"
x-data="{ csrfError: false }"
@submit.prevent="
const formData = new FormData($el);
const alreadyCleaned = window.location.search.includes('cleaned=1');
fetch($el.action, {
method: 'POST',
body: formData,
credentials: 'same-origin',
headers: { 'Accept': 'text/html,application/xhtml+xml' }
})
.then(response => {
if (response.status === 419) {
// CSRF token mismatch
if (alreadyCleaned) {
// Cleanup zaten yapıldı (sonsuz döngü önleme)
window.location.href = '/login';
return;
}
csrfError = true;
window.location.href = '/session-cleanup';
return;
}
if (response.redirected) {
window.location.href = response.url;
return;
}
return response.text().then(html => {
document.open();
document.write(html);
document.close();
});
})
.catch(error => console.error('Login error:', error));
">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<!-- ... form içeriği ... -->
</form>
// SESSION CLEANUP - 419 kısır döngüsünü kırmak için
// Tüm eski session cookie'lerini temizler ve login'e yönlendirir
Route::get('/session-cleanup', [\App\Http\Controllers\SessionCleanupController::class, 'cleanup'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class])
->name('session.cleanup');
# Fake legacy cookie ile istek at
curl -I "https://www.example.com/" -H "Cookie: laravel_session_1001=fake123"
# Response'ta şunu görmelisin:
# set-cookie: laravel_session_1001=deleted; expires=Thu, 01 Jan 1970...; Max-Age=0
laravel_session_1001=fake123 cookie'si ekleSet-Cookie: laravel_session_1001=...; Max-Age=0 gör.example.com domain'lerini kendi domain'inizle değiştirinphp artisan cache:clear && php artisan config:clear