CSRF (cross-site request forgery, siteler arası istek sahteciliği), saldırganın kendi sitesinden kullanıcının tarayıcısına hedef siteye bir HTTP isteği yaptırmasıdır. Tarayıcı bu isteğe kullanıcının hedef sitedeki oturum çerezini kendiliğinden eklediği için sunucu isteği gerçek kullanıcıdan geldi sanır ve para transferi, e-posta değiştirme ya da sipariş gibi zararlı bir işlemi yapar. Korunmanın üç ana yolu vardır: sayfaya gömülü tahmin edilemez bir CSRF tokeni, tarayıcının eklediği Fetch Metadata başlıklarıyla (Sec-Fetch-Site) çapraz site isteklerini reddetmek ve JavaScript'ten gönderilen durum değiştiren isteklerin basit istek olmamasını sağlamak. SameSite çerez özniteliği bunlara ek bir derinlemesine savunma katmanıdır.

Bu yazı MDN Web Güvenliği bölümündeki CSRF sayfasını ve Fetch Metadata kılavuzunu temel alıyor; saldırının nasıl çalıştığını, her savunmanın neyi engellediğini ve nerede yetersiz kaldığını, ayrıca token üretimi ile sunucu tarafında bir karar fonksiyonu örneğini içeriyor. XSS ve CSP XSS ve CSP koruması yazısında, tıklama ele geçirme clickjacking yazısında, çerez özniteliklerinin tamamı document.cookie ve Set-Cookie yazısında anlatılıyor; serinin haritası Web API öğrenme rehberi sayfasında.

CSRF saldırısı nasıl çalışır?

Bir web sitesi kullanıcı adına ürün satın alma, randevu alma ya da para gönderme gibi özel işlemleri, kullanıcının tarayıcısından gelen bir HTTP isteğiyle yapar. İsteğin gerçekten o kullanıcıdan geldiğinden emin olmak için sunucu isteğin içinde kimlik bilgisi bekler; çoğu zaman bu, oturum kimliğini taşıyan bir çerezdir. MDN bu tür isteklere durum değiştiren istek der.

MDN'in örneğinde kullanıcı bankasına daha önce giriş yapmıştır ve tarayıcıda bir oturum çerezi saklıdır. Saldırgan kendi sitesine, action özniteliği bankanın transfer adresini gösteren ve bankanın alanlarını taklit eden gizli girişler içeren bir form koyar, sonra bu formu sayfa yüklenince JavaScript ile gönderir:

html
<form action="https://my-bank.example.org/transfer" method="POST">
  <input type="hidden" name="recipient" value="attacker" />
  <input type="hidden" name="amount" value="1000" />
</form>
javascript
const form = document.querySelector("form");
form.submit();

Kullanıcı bu sayfayı açtığında tarayıcı formu bankaya gönderir. Kullanıcı bankaya giriş yapmış olduğu için istek gerçek çerezi taşıyabilir; sunucu çerezi doğrular ve parayı aktarır. İşlem GET ile yapılıyorsa saldırganın forma bile ihtiyacı yoktur: sayfaya, kaynağı transfer adresi olan bir <img> koymak yeter, tarayıcı görseli yüklemeye çalışırken aslında işlemi tetikler.

html
<img
  src="https://my-bank.example.org/transfer?recipient=attacker&amount=1000" />

MDN'e göre bir sitenin CSRF'e açık olması için üç koşul bir arada bulunur: sunucuda durum değiştirmek için HTTP istekleri kullanılır, isteğin oturum açmış kullanıcıdan geldiği yalnızca çerezle doğrulanır ve istek yalnızca saldırganın tahmin edebileceği parametreler içerir. Savunmaların her biri bu koşullardan birini ortadan kaldırır. Dikkat edin, saldırgan yanıtı okuyamaz; amacı sunucuya bir yan etki yaptırmaktır. Yanıtın okunmasını CORS ve aynı köken politikası engeller, ama isteğin gönderilmesini her zaman engellemez.

Savunma 1: CSRF tokeni

Sunucu bir sayfayı sunarken sayfaya tahmin edilemez bir değer, yani CSRF tokeni gömer. Meşru sayfa durum değiştiren isteği gönderirken bu tokeni de ekler; sunucu tokeni kontrol eder ve yalnızca eşleşirse işlemi yapar. Saldırgan token değerini tahmin edemediği için geçerli bir sahte istek kuramaz. MDN, saldırgan kullanılmış bir tokeni bir şekilde öğrense bile token her seferinde değişiyorsa isteğin yeniden oynatılamayacağını da ekler.

Formlarda token genellikle gizli bir form alanına konur ve form gönderilince kendiliğinden sunucuya döner. fetch() gibi JavaScript API'lerinde token bir çerezde ya da sayfaya gömülü bir yerde durabilir; betik değeri okuyup ek bir istek başlığı olarak gönderir. MDN, modern web çatılarının çoğunda CSRF tokeni desteğinin hazır geldiğini (örneğin Django'nun csrf_token etiketi) ve bu korumadan yararlanmak için sitenizdeki tüm durum değiştiren istekleri bilmeniz gerektiğini vurgular.

html
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="SUNUCUNUN_URETTIGI_DEGER" />
  <label>Alıcı <input name="recipient" /></label>
  <label>Tutar <input name="amount" type="number" /></label>
  <button>Gönder</button>
</form>

Tokenin işe yaraması tahmin edilemez olmasına bağlıdır; bu yüzden Math.random() yerine kriptografik olarak güçlü bir rastgele kaynak kullanılmalıdır. Aşağıdaki örnek crypto.getRandomValues() ile 32 baytlık bir token üretir ve onaltılık dizeye çevirir. Kod tarayıcıda ve Node.js'te aynı çalışır; gerçek uygulamada tokeni sunucu üretir ve oturumla ilişkilendirir.

javascript
function csrfTokeni() {
  const bayt = crypto.getRandomValues(new Uint8Array(32));
  return Array.from(bayt, (b) => b.toString(16).padStart(2, '0')).join('');
}

const t1 = csrfTokeni();
const t2 = csrfTokeni();
console.log(t1.length, /^[0-9a-f]+$/.test(t1), t1 === t2); // 64 true false

crypto.getRandomValues() ve crypto.randomUUID() ayrıntıları Web Crypto API yazısında. Sayfadaki betikten tokeni bir başlıkla göndermek ise şöyle görünür; burada tokenin sayfadaki bir meta etiketinden okunduğunu varsayıyoruz:

javascript
const token = document.querySelector('meta[name="csrf-token"]').content;

await fetch('/transfer', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
  body: JSON.stringify({ recipient: 'joe', amount: '100' }),
});

Savunma 2: Fetch Metadata başlıkları

Savunma 2: Fetch Metadata başlıkları: Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest, Sec-Fetch-User

Fetch Metadata, tarayıcının isteğe eklediği ve isteğin bağlamını anlatan dört başlıktan oluşur: Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest ve Sec-Fetch-User. Sec- önekli tüm başlıklar gibi bunlar da yasaklı istek başlıklarıdır, yani sayfadaki kod onları ayarlayamaz ya da değiştiremez. Bu başlıklar yalnızca potansiyel olarak güvenilir URL'lere giden isteklerde gönderilir; yani güvenli olmayan http:// kökenlerindeki sunucular bu başlıkları almaz. Güvenli bağlam kavramı için güvenli bağlam ve DOMException yazısına, HTTPS kurulumu için HTTPS ve TLS rehberi yazısına bakın.

BaşlıkNe söylerÖrnek değerler
Sec-Fetch-Siteİsteği başlatanın kökeni ile istenen kaynağın ilişkisisame-origin, same-site, cross-site, none
Sec-Fetch-Modeİsteğin modu (Request.mode)navigate, cors, no-cors, same-origin, websocket
Sec-Fetch-DestYanıtın nerede kullanılacağı (Request.destination)document, image, script, iframe, empty
Sec-Fetch-Userİstek kullanıcı etkinliğiyle mi başladıYalnızca ?1, yoksa başlık hiç gönderilmez

CSRF için en önemlisi Sec-Fetch-Site'dır. MDN'in örneğinde https://books.example.org/authors sayfasındaki bir bağlantı https://books.example.org/titles adresine giderse değer same-origin, https://login.example.org/ adresine giderse same-site, https://books.example.com/titles adresine giderse cross-site olur. Adres çubuğuna yazılan URL veya yer imi gibi doğrudan kullanıcının başlattığı isteklerde değer none'dır. Aynı eşleme form gönderimleri, görsel ve betik gibi alt kaynaklar ve fetch() çağrıları için de geçerlidir. MDN, kabul edilmeyen isteklerin varsayılan olarak 403 ile reddedilmesini önerir.

Kaynak yalıtım politikası

Tüm çapraz köken istekleri reddetmek, başka sitelerdeki bağlantılardan sitenize gelinmesini de engeller. MDN'in önerdiği kaynak yalıtım politikası yalnızca şunlara izin verir: aynı köken istekleri (alt alan adlarınıza güveniyorsanız aynı site istekleri de), kullanıcıların başka sitelerdeki bağlantılara tıklayarak gelebilmesi için başka kökenden gelen üst düzey gezinme istekleri ve CORS kullananlar dahil bilerek çapraz köken erişime açtığınız uç noktalar. MDN'in Express örneği Sec-Fetch-Site değeri same-origin veya none olanları ve Sec-Fetch-Mode: navigate taşıyan GET isteklerini kabul eder, gerisini reddeder. Yanıta Vary: sec-fetch-site, sec-fetch-mode eklenir ki önbelleğe alınan bir yanıt farklı Fetch Metadata değerli isteklere sunulmasın; Vary başlığının mantığı Cache-Control ve Vary yazısında.

Savunma 3: Durum değiştiren istekleri basit istek yapmamak

Savunma 3: Durum değiştiren istekleri basit istek yapmamak

Tarayıcılar HTTP isteklerini basit istekler ve diğerleri olarak ikiye ayırır. Bir <form> gönderiminin ürettiği türden basit istekler, web'in ilk günlerinden beri olduğu gibi engellenmeden başka kökenlere gönderilebilir; uyumluluk için bu değişmez ve formlar bu yüzden token gibi başka savunmalara ihtiyaç duyar. MDN'in CORS kılavuzu basit isteği şöyle tanımlar: metot GET, HEAD veya POST; elle ayarlanan başlıklar yalnızca CORS güvenli listesindekiler (Accept, Accept-Language, Content-Language, Content-Type, tek aralıklı Range); Content-Type yalnızca application/x-www-form-urlencoded, multipart/form-data veya text/plain; ayrıca istekte ReadableStream kullanılmamış olmalı.

fetch() gibi API'ler ise özel başlık içeren istekler yapabilir ve bu istekler varsayılan olarak başka kökenlere gönderilmez; tarayıcı önce bir ön kontrol yapar. Bu yüzden durum değiştiren isteklerini fetch() veya XMLHttpRequest ile gönderen bir site, bu isteklerin hiçbir zaman basit istek olmamasını sağlayarak CSRF'e karşı korunabilir. MDN iki yol gösterir: Content-Type değerini application/json yapmak ya da standart başlıklarla çakışmayan özel bir başlık eklemek. Sunucu bu başlığın varlığını kontrol eder; başlık varsa istek basit istek olarak işlenmemiş demektir.

javascript
fetch("https://my-bank.example.org/transfer", {
  method: "POST",
  headers: {
    "X-MY-BANK-ANTI-CSRF": 1,
  },
  body: JSON.stringify({ recipient: "joe", amount: "100" }),
});

Origin başlığı ile kontrol

Origin istek başlığı, isteğe neden olan kökeni (şema, ana makine adı ve port) bildirir. Referer başlığına benzer, ancak yolu açığa çıkarmaz ve null olabilir. MDN'e göre tarayıcılar bu başlığı genel olarak çapraz köken isteklerine ve GET ile HEAD dışındaki aynı köken isteklerine (yani aynı köken POST, PUT, PATCH, DELETE, OPTIONS) ekler. Bu yüzden Sec-Fetch-Site göndermeyen istemciler için, durum değiştiren bir istekte Origin değerini beklediğiniz kökenle karşılaştırmak yararlı bir yedek kontroldür.

Ancak değerin null olabileceği durumlar çoktur: file:, data:, blob: gibi şemalar, kökenler arası yönlendirmeler, allow-same-origin içermeyen sandbox'lı iframe'ler ve bazı Referrer-Policy değerleriyle yapılan basit form gönderimleri bunlardan bazılarıdır. null değerini kendi kökeninizle eşleşmiş saymamalısınız.

Örnek: sunucu tarafında karar fonksiyonu

Aşağıdaki fonksiyon, MDN'in önerdiği kontrolleri tek yerde birleştirir: güvenli metotları geçirir, Sec-Fetch-Site varsa yalnızca same-origin kabul eder, yoksa Origin başlığına bakar, son olarak tokeni karşılaştırır. Fonksiyon sadece method ve headers alanlarını okuduğu için Fetch standardındaki Request nesnesini kullanan sunucu ortamlarına uyarlanabilir; burada denemek için düz nesneler ve Headers kullanıldı. Bu bir öğretim örneğidir, gerçek projede çatınızın hazır CSRF korumasını tercih edin.

javascript
const GUVENLI = new Set(['GET', 'HEAD', 'OPTIONS']);
const KENDI_KOKENIM = 'https://banka.example';

function izinVer(istek, oturumTokeni) {
  if (GUVENLI.has(istek.method)) return 'izin: güvenli metot';

  const site = istek.headers.get('Sec-Fetch-Site');
  if (site !== null) {
    if (site !== 'same-origin') return 'red: Sec-Fetch-Site=' + site;
  } else {
    const koken = istek.headers.get('Origin');
    if (koken !== KENDI_KOKENIM) return 'red: Origin=' + koken;
  }

  if (istek.headers.get('X-CSRF-Token') !== oturumTokeni) return 'red: token eşleşmedi';
  return 'izin: aynı köken ve geçerli token';
}

const token = 'a1b2c3';
const ornekler = [
  { method: 'GET', headers: new Headers({ 'Sec-Fetch-Site': 'cross-site' }) },
  { method: 'POST', headers: new Headers({ 'Sec-Fetch-Site': 'cross-site', 'X-CSRF-Token': token }) },
  { method: 'POST', headers: new Headers({ 'Sec-Fetch-Site': 'same-site', 'X-CSRF-Token': token }) },
  { method: 'POST', headers: new Headers({ 'Sec-Fetch-Site': 'same-origin' }) },
  { method: 'POST', headers: new Headers({ 'Sec-Fetch-Site': 'same-origin', 'X-CSRF-Token': token }) },
  { method: 'POST', headers: new Headers({ Origin: 'null', 'X-CSRF-Token': token }) },
  { method: 'POST', headers: new Headers({ Origin: 'https://banka.example', 'X-CSRF-Token': token }) },
];
for (const istek of ornekler) console.log(izinVer(istek, token));
// izin: güvenli metot
// red: Sec-Fetch-Site=cross-site
// red: Sec-Fetch-Site=same-site
// red: token eşleşmedi
// izin: aynı köken ve geçerli token
// red: Origin=null
// izin: aynı köken ve geçerli token

Çıktıdaki satırları sırayla okuyalım. Çapraz siteden gelse bile GET geçirildi; bu yüzden GET ile hiçbir zaman durum değiştirmemelisiniz. Doğru tokeni taşısa bile cross-site ve same-site istekler reddedildi; alt alan adlarınıza güveniyorsanız same-site değerini de kabul edecek şekilde gevşetebilirsiniz. Aynı kökenden gelen ama tokeni olmayan istek reddedildi. Sec-Fetch-Site içermeyen isteklerde Origin: null reddedilirken kendi kökeniniz kabul edildi. Ayrıca X-CSRF-Token özel başlığı, tarayıcıdan gelen isteği basit istek olmaktan çıkardığı için üçüncü savunmayı da kendiliğinden devreye sokar.

Derinlemesine savunma: SameSite çerezleri

Derinlemesine savunma: SameSite çerezleri: Strict, Yalnızca üst düzey gezinme ve güvenli metotla (GET gibi) eklenir, None

SameSite çerez özniteliği tarayıcının çerezi çapraz site isteklerine ne zaman ekleyeceğini belirler ve üç değeri vardır: None, Lax ve Strict. MDN bunu tam bir savunma olarak değil, diğer yöntemlerden birine eklenen bir katman olarak görür.

DeğerÇapraz site isteklerinde çerezSonuç
StrictHiçbir çapraz site isteğine eklenmezEn güçlü koruma; başka siteden bağlantıyla gelen kullanıcı tanınmaz
LaxYalnızca üst düzey gezinme ve güvenli metotla (GET gibi) eklenirBağlantılar çalışır; koruma Strict'ten belirgin biçimde zayıf
NoneTüm isteklere eklenirCSRF'e karşı koruma sağlamaz

MDN Lax'ın iki zayıflığına dikkat çeker. Saldırgan bir üst düzey gezinmeyi tetikleyebilir; yazının başındaki form gönderimi de bir üst düzey gezinme sayılır ve form GET ile gönderilirse SameSite=Lax çerezler isteğe eklenir. Ayrıca bazı çatılar "metot geçersiz kılma" (method override) destekler; saldırgan GET ile gönderdiği isteği sunucuya POST gibi gösterebilir. Bu nedenle öneri, oturum açmış kullanıcıya sayfa gösterip göstermemeye karar verdiğiniz çerezler için Lax, çapraz siteden izin vermek istemediğiniz durum değiştiren istekler için kullandığınız çerezler için Strict kullanmaktır.

Bir diğer sınır, SameSite'ın farklı kökene değil farklı siteye karşı koruma sağlamasıdır. https://foo.example.org ve https://bar.example.org farklı kökenlerdir ama aynı sitedir; aynı site korumasına güveniyorsanız tüm alt alan adlarınıza güvenmeniz gerekir. MDN'in güvenli çerez kılavuzu ayrıca __Host- veya __Secure- önekini, Secure ve HttpOnly özniteliklerini ve oturum kimliklerinin olabildiğince kısa sürede sona ermesini önerir.

http
Set-Cookie: __Host-BMOSESSIONID=YnVnemlsbGE=; Max-Age=2592000; Path=/; Secure; HttpOnly; SameSite=Strict

Savunmaları karşılaştırma

SavunmaEn uygun olduğu yerSınırı
CSRF tokeniHTML formlarıyla yapılan durum değiştiren isteklerTüm durum değiştiren istekleri kapsamalı; çatının desteğini kullanın
Fetch MetadataHer türlü istek, sunucu tarafında genel politikaYalnızca güvenilir (HTTPS) URL'lere gönderilir; başlığı göndermeyen istemciler için yedek gerekir
Basit istek olmamakfetch veya XMLHttpRequest ile gönderilen isteklerKimlik bilgili gevşek CORS ayarı korumayı bozar
Origin kontrolüFetch Metadata yokken yedek kontrolDeğer null olabilir
SameSite çerezEk katmanLax zayıftır; farklı köken değil farklı site korur

Sık yapılan hatalar

  • Durum değiştiren işlemleri GET ile yapmak. <img> ile tetiklenebilir ve SameSite=Lax çerezler üst düzey GET gezinmelerinde gönderilir. Metotların anlamı için HTTP metotları yazısına bakın.
  • Yalnızca SameSite'a güvenmek. MDN bunu tam savunma değil, derinlemesine savunma olarak tanımlar.
  • Tahmin edilebilir token kullanmak. Kullanıcı kimliği, zaman damgası ya da Math.random() tabanlı değerler tahmin edilebilir; kriptografik rastgelelik kullanın.
  • Tokeni bazı uç noktalarda unutmak. Koruma yalnızca uygulandığı isteklerde geçerlidir; tüm durum değiştiren istekleri listeleyin.
  • Origin: null değerini kabul etmek. Sandbox'lı iframe ve data: URL'leri gibi durumlarda bu değer saldırganın kontrolündeki sayfalardan da gelebilir.
  • Kimlik bilgili CORS iznini geniş tutmak. İstek kökenini düşünmeden Access-Control-Allow-Origin içine yansıtmak ve Access-Control-Allow-Credentials göndermek basit istek savunmasını boşa çıkarır.

Kontrol listesi

  • Oturum çereziyle kullanıcıyı tanıyan tüm durum değiştiren istekleri belirleyin.
  • Formlar için CSRF tokeni destekleyen bir çatı kullanın ve bu desteği etkinleştirin.
  • fetch() veya XMLHttpRequest ile gönderilen durum değiştiren isteklerin basit istek olmadığından emin olun.
  • Hangi yöntemi kullanırsanız kullanın, çapraz site istekleri Fetch Metadata ile reddetmeyi düşünün.
  • Durum değiştirmek için GET kullanmayın.
  • Oturum çerezlerinde mümkünse SameSite=Strict, gerekiyorsa Lax kullanın.

Sık Sorulan Sorular

CORS CSRF'i engeller mi?

Tek başına hayır. CORS, betiğin başka kökenden gelen yanıtı okuyup okuyamayacağını denetler; basit istekler ise ön kontrol olmadan gönderilir. Bir form gönderimi CORS ayarı ne olursa olsun sunucuya ulaşır. CORS, yalnızca basit olmayan istekleri kullandığınızda ve kimlik bilgili izinleri sıkı tuttuğunuzda bir savunma parçası olur.

Tarayıcım Sec-Fetch-Site göndermiyorsa ne olur?

Başlık yalnızca potansiyel olarak güvenilir URL'lere giden isteklerde gönderilir ve tüm istemciler göndermeyebilir. Bu yüzden başlık yoksa Origin kontrolü ya da token gibi başka bir savunmaya düşen bir yedek yol tasarlayın.

Tokeni çerezde mi yoksa sayfada mı tutmalıyım?

MDN, formlarda gizli alanı, fetch() için çerezde ya da sayfaya gömülü bir değeri örnek gösterir; önemli olan, betiğin bu değeri okuyup ayrı bir başlık ya da alan olarak göndermesi ve sunucunun karşılaştırmasıdır. Çatınız hangi yöntemi destekliyorsa onu izleyin.

API'm yalnızca JSON kabul ediyor, yine de korunmam gerekir mi?

Content-Type: application/json zorunluysa tarayıcıdan gelen bu istekler basit istek olmaz ve MDN bunu bir savunma olarak sayar. Ancak sunucunuzun içerik türünü gerçekten zorunlu tuttuğundan ve kimlik bilgili CORS iznini geniş vermediğinizden emin olun; Fetch Metadata kontrolü eklemek ek güvence sağlar.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · XSS ve CSP koruması · Clickjacking koruması · document.cookie ve Set-Cookie · CORS nedir · HTTP metotları · Web Crypto API