Clickjacking (tıklama ele geçirme), saldırganın hedef siteyi kendi tuzak sayfasında görünmez bir <iframe> içine yerleştirip kullanıcıyı farkında olmadan o sitedeki bir düğmeye tıklatmasıdır. Korunmanın ana yolu sayfanızın başka siteler tarafından çerçevelenmesini engellemek ya da sınırlamaktır: HTTP yanıtına Content-Security-Policy: frame-ancestors 'none' (veya izin verdiğiniz kökenlerin listesi) ve eski tarayıcılar için X-Frame-Options: DENY ya da SAMEORIGIN eklenir. Oturum çerezlerinde SameSite=Lax veya Strict ek bir kısmi önlemdir.

Bu yazı yalnızca çerçeveleme korumasını anlatıyor: saldırının nasıl kurulduğu, iki başlığın sözdizimi ve farkları, birlikte kullanıldıklarında hangisinin geçerli olduğu, iç içe çerçevelerde ata kontrolü ve sunucu yapılandırması. İçerik Güvenlik Politikası'nın (CSP) betik, nonce ve diğer yönergeleri XSS ve CSP koruması yazısında, kullanıcının oturumuyla sahte istek gönderilmesi ise CSRF saldırısı yazısında ele alınıyor. Serinin tamamı için Web API öğrenme rehberi sayfasına bakın.

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

Saldırgan, kullanıcının hedef sitesini bir <iframe> içine gömen bir tuzak sayfa hazırlar. Çerçeveyi görünmez yapar ve kendi tuzak öğelerini, hedef sitede hassas bir işlemi tetikleyen öğelerle tam aynı yere hizalar. Kullanıcı tuzak öğeyle etkileşime girdiğini sanırken aslında görünmeyen hedef siteye tıklar ve istemediği bir işlemi yapmış olur.

MDN'in örneğinde kullanıcının bankası https://my-bank.example.com adresindedir ve kullanıcı genellikle oturumu açık bırakır. Basitlik için bankanın bir sayfasında tüm parayı aktaran bir düğme olduğu varsayılır; gerçekçi olmasa da sitelerde hassas işlem yapan etkileşimli öğeler bulunması olasıdır. Saldırgan, kullanıcıyı tıklamaya özendiren bir düğme ve bankanın sayfasını gömen bir <iframe> içeren bir sayfa yapar. CSS ile çerçevenin opaklığını sıfıra çeker ve düğmeyi bankadaki transfer düğmesinin üzerine konumlandırır.

html
<style>
  iframe {
    opacity: 0;
  }
  #fake-button {
    position: absolute;
    top: 185px;
    left: 90px;
  }
</style>
<button id="fake-button">Click here for a free kitten!</button>
<iframe width="800" height="200" src="https://my-bank.example.com"></iframe>

Kullanıcı ücretsiz kedi vaat eden düğmeye tıkladığında aslında bankanın görünmeyen transfer düğmesine tıklamış olur. Kullanıcı oturum açmışsa bu tıklamanın ürettiği istek gerçek kimlik bilgilerini taşır ve başarılı olur. CSRF'ten farkı şudur: CSRF'te saldırgan isteği kendisi kurar, clickjacking'de ise isteği hedef sitenin kendi arayüzü üretir; bu yüzden form içine gömülü CSRF tokenleri burada koruma sağlamaz, çünkü tıklanan form zaten gerçek sitenin geçerli tokenli formudur.

Çerçeveleme yalnızca tıklama hilesi için değil, bazı siteler arası sızıntılar (cross-site leaks) için de kullanılır. MDN'in XS-Leaks sayfası, saldırganın hedef sayfayı çerçeveleyip contentWindow üzerinden window.length değerini okuyarak sayfadaki çerçeve sayısını, dolayısıyla kullanıcının oturum durumu gibi bilgileri öğrenebildiğini anlatır. Bu nedenle MDN, gerekmedikçe sitenin gömülebilir olmamasını iyi bir uygulama olarak önerir.

Ana savunma: gömülmeyi kısıtlamak

Ana savunma: gömülmeyi kısıtlamak: CSP frame-ancestors, X-Frame-Options

Clickjacking, hedef sitenin saldırganın sayfasında bir <iframe> içine gömülebilmesine dayanır. Bu yüzden asıl savunma bu yeteneği kapatmak ya da en azından sınırlamaktır. Bunun için iki araç vardır ve ikisi de HTTP yanıt başlığı olarak gönderilir:

AraçNe kadar ayrıntılıDeğerler
CSP frame-ancestorsHangi belgelerin sizi gömebileceğini tek tek belirlersiniz'none', 'self', ana makine kaynakları, şema kaynakları
X-Frame-OptionsYa tamamen kapatır ya da yalnızca aynı kökene izin verirDENY, SAMEORIGIN (ALLOW-FROM eskimiş)

frame-ancestors, X-Frame-Options'ın yerine geçen yönergedir. MDN'e göre iki başlık birlikte gönderildiğinde frame-ancestors destekleyen tarayıcılar X-Frame-Options'ı yok sayar; ikisini birlikte göndermek, frame-ancestors desteklemeyen tarayıcılarda da gömülmeyi engeller. MDN, frame-ancestors için tarayıcı desteğinin çok iyi olduğunu, bu yüzden bunun büyük bir endişe olmadığını da ekler; ayrıntı için MDN uyumluluk tablosuna bakın.

CSP frame-ancestors yönergesi

frame-ancestors, bir sayfayı <frame>, <iframe>, <object> veya <embed> ile gömebilecek geçerli ebeveynleri belirtir. Değer ya tek başına 'none' anahtar kelimesidir ya da boşlukla ayrılmış kaynak ifadeleri listesidir. Bu yönergede kullanılabilen kaynak ifadeleri yalnızca ana makine kaynakları (https://www.example.org gibi), şema kaynakları (https: gibi) ve 'self' anahtar kelimesidir. 'none' ve 'self' yazılırken tek tırnaklar zorunludur; başlığı kopyalarken tırnakların düşmediğinden emin olun.

http
Content-Security-Policy: frame-ancestors 'none';

Content-Security-Policy: frame-ancestors 'self' https://www.example.org;

Content-Security-Policy: frame-ancestors 'self' https://example.org https://example.com https://store.example.com;

MDN'in CSP kılavuzu, sitenizin gömülebilir olması gerekmiyorsa frame-ancestors değerini 'none' yapmanızı önerir. Belirli bir ortağın sizi gömmesine izin vermeniz gerekiyorsa listeyi olabildiğince dar tutun; https: gibi bir şema kaynağı tüm HTTPS sitelerine izin vereceği için korumayı neredeyse anlamsız kılar.

default-src frame-ancestors yerine geçmez

Sık atlanan bir kural: frame-ancestors sözdizimi diğer yönergelerin kaynak listesine benzese de default-src değerine geri düşmez. MDN, default-src 'none' bildiren bir politikanın kaynağın herkes tarafından gömülmesine hâlâ izin verdiğini açıkça belirtir. Sıkı bir CSP yazdığınızı düşünüp frame-ancestors eklemeyi unutursanız sayfanız çerçevelenmeye açık kalır.

İç içe çerçevelerde tüm atalar kontrol edilir

frame-ancestors yalnızca doğrudan ebeveyni değil, zincirdeki her atayı kontrol eder; herhangi biri eşleşmezse yükleme iptal edilir. Yani sayfanız izin verdiğiniz https://ortak.example içinde gömülü olsa bile, o sayfa da başka bir sitenin içine gömülmüşse ve o site listenizde yoksa sayfanız yüklenmez. İç içe çerçeve kullanan bir yapı kuruyorsanız en içteki çerçevenin politikasında tüm ataları listelemeniz gerekir.

frame-ancestors ile frame-src farkı

İki yönerge sık karıştırılır. frame-ancestors sayfanızı kimlerin gömebileceğini belirler ve clickjacking'e karşı koruma sağlar. frame-src ise sayfanızın içindeki çerçevelerin nereden yüklenebileceğini belirler; sizin gömdüğünüz içeriği sınırlar, başkalarının sizi gömmesini engellemez.

X-Frame-Options başlığı

X-Frame-Options başlığı: DENY, SAMEORIGIN, ALLOW-FROM origin

X-Frame-Options, tarayıcının belgeyi bir <frame>, <iframe>, <embed> veya <object> içinde gösterip gösteremeyeceğini bildirir. MDN, bu başlık gönderilmezse ve frame-ancestors gibi başka bir mekanizma da yoksa tarayıcının diğer sitelerin belgeyi gömmesine izin vereceğini hatırlatır; yani varsayılan durum korumasızdır.

DeğerAnlamı
DENYBelge kökene bakılmaksızın hiçbir çerçevede yüklenemez; aynı kökenden gömme de engellenir
SAMEORIGINBelge yalnızca tüm ata çerçeveler sayfayla aynı kökendeyse gömülebilir
ALLOW-FROM originEskimiş yönerge; modern tarayıcılar bu değeri görünce başlığın tamamını yok sayar

Bir diğer tuzak <meta> etiketidir. MDN, <meta http-equiv="X-Frame-Options" content="deny"> biçiminde verilen değerin hiçbir etkisi olmadığını, X-Frame-Options'ın yalnızca HTTP başlığıyla uygulandığını vurgular. CSP için de MDN, <meta> ile verilen politikaların tüm CSP özelliklerini desteklemediğini belirtir; çerçeveleme korumasını HTTP yanıt başlığıyla göndermek en güvenli yoldur.

Sunucuda başlıkları ayarlamak

MDN, X-Frame-Options başlığının yaygın sunucularda nasıl ayarlanacağını örneklerle gösterir. Nginx'te başlığı http, server veya location bloğuna ekleyebilirsiniz; always parametresi başlığın hata yanıtlarında da gönderilmesini sağlar. Aynı yere CSP başlığını da ekleyerek iki katmanı birlikte kurabilirsiniz:

nginx
add_header X-Frame-Options DENY always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;

Apache için MDN'in verdiği yapılandırma şöyledir; CSP satırını aynı biçimde ekleyebilirsiniz:

apache
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"

MDN ayrıca IIS için Web.config içinde özel başlık, HAProxy için http-response set-header ve Express için Helmet ara katmanının xFrameOptions seçeneği örneklerini verir. Uygulamanız zaten bir Content-Security-Policy başlığı gönderiyorsa yeni bir başlık eklemek yerine mevcut politikaya frame-ancestors yönergesini eklemek daha düzenlidir; birden fazla CSP başlığı gönderildiğinde politikalar yalnızca daha da kısıtlayıcı hale gelebilir. Sunucunuzun HTTPS üzerinden çalıştığından emin olmak için HTTPS ve TLS rehberi yazısına bakabilirsiniz.

Örnek: bir yanıtın gömülüp gömülemeyeceğini değerlendirmek

Aşağıdaki fonksiyon, yanıt başlıklarına ve ata çerçevelerin kökenlerine bakarak tarayıcının sayfayı yükleyip yüklemeyeceğini yaklaşık olarak hesaplar. MDN'deki üç kuralı somutlaştırır: frame-ancestors varsa X-Frame-Options yok sayılır, tüm atalar listeyle eşleşmelidir ve default-src çerçevelemeyi sınırlamaz. Gerçek tarayıcı eşleştirmesi joker karakterli alan adları, portlar ve yollar gibi daha fazla ayrıntı içerir; bu kod yalnızca politikalarınızı düşünmek için bir öğretim aracıdır ve tarayıcıda da Node.js'te de aynı çalışır.

javascript
function frameAncestors(csp) {
  for (const yonerge of csp.split(';')) {
    const [ad, ...degerler] = yonerge.trim().split(/\s+/);
    if (ad.toLowerCase() === 'frame-ancestors') return degerler;
  }
  return null;
}

function eslesir(kaynak, ata, sayfa) {
  if (kaynak === "'self'") return ata === sayfa;
  if (kaynak.endsWith(':')) return new URL(ata).protocol === kaynak;
  return new URL(kaynak).origin === ata;
}

function gomulebilirMi(basliklar, sayfaKokeni, atalar) {
  const liste = frameAncestors(basliklar.get('Content-Security-Policy') ?? '');
  if (liste !== null) {
    if (liste.includes("'none'")) return false;
    return atalar.every((ata) => liste.some((k) => eslesir(k, ata, sayfaKokeni)));
  }
  const xfo = (basliklar.get('X-Frame-Options') ?? '').toUpperCase();
  if (xfo === 'DENY') return false;
  if (xfo === 'SAMEORIGIN') return atalar.every((ata) => ata === sayfaKokeni);
  return true;
}

const sayfa = 'https://panel.example';
const h1 = new Headers({ 'Content-Security-Policy': "default-src 'self'; frame-ancestors 'self' https://ortak.example" });
console.log(gomulebilirMi(h1, sayfa, ['https://ortak.example'])); // true
console.log(gomulebilirMi(h1, sayfa, ['https://ortak.example', 'https://kotu.example'])); // false
console.log(gomulebilirMi(h1, sayfa, ['https://panel.example'])); // true

const h2 = new Headers({ 'X-Frame-Options': 'SAMEORIGIN', 'Content-Security-Policy': 'frame-ancestors https://ortak.example' });
console.log(gomulebilirMi(h2, sayfa, ['https://ortak.example'])); // true

const h3 = new Headers({ 'X-Frame-Options': 'DENY' });
console.log(gomulebilirMi(h3, sayfa, ['https://panel.example'])); // false

const h4 = new Headers({ 'Content-Security-Policy': "default-src 'none'" });
console.log(gomulebilirMi(h4, sayfa, ['https://kotu.example'])); // true

Sonuçları okuyalım. İlk politikada https://ortak.example izinli olduğu için tek atalı gömme kabul edildi; aynı ortak sayfa izinsiz bir sitenin içine gömüldüğünde ise ikinci ata eşleşmediği için reddedildi. 'self' sayesinde aynı kökenden gömme kabul edildi. İkinci başlık kümesinde X-Frame-Options: SAMEORIGIN ortak siteyi engellerdi, ama frame-ancestors bulunduğu için yok sayıldı ve sonuç true oldu. DENY aynı kökenden gömmeyi bile engelledi. Son satır, yalnızca default-src 'none' içeren bir politikanın sayfayı herhangi bir siteye karşı korumadığını gösteriyor.

Ek önlem: SameSite çerezleri

MDN, ek ve kısmi bir önlem olarak oturum çerezlerinde SameSite özniteliğinin Lax veya Strict yapılmasını önerir. Üst düzey belgeyle aynı sitede olmayan bir <iframe> gibi gömülü bağlamlardan yapılan istekler bu çerezleri taşımaz; sunucu da isteği oturum açmış bir kullanıcıdan gelmiş saymaz. Böylece gömme engeli bir şekilde aşılsa bile tıklama, kullanıcının oturumuyla işlem yapamaz. Ancak bu tek başına yeterli değildir: aynı site sayılan bir alt alan adınız saldırganın eline geçerse bu koruma devreye girmez. Çerez özniteliklerinin ayrıntıları Set-Cookie öznitelikleri yazısında.

http
Set-Cookie: __Host-SID=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

Gömülmeye izin vermeniz gerekiyorsa

Bazı sayfaların başka sitelerde gösterilmesi gerekir: harita ya da video oynatıcı gibi gömülebilir bileşenler, ödeme widget'ları veya iş ortaklarına sunulan paneller. Bu durumda yalnızca gömülmesi gereken yolları açın; sitenin geri kalanı 'none' ile korunmaya devam etsin. Gömülebilir sayfalarda kullanıcı adına hassas işlem yapan düğmeler bulundurmamak, bu tür işlemler için ayrı ve gömülemeyen bir sayfaya yönlendirmek de riski azaltır. İzinli kökenleri bir yapılandırma listesinden okuyup frame-ancestors değerine yazmak, istekteki bir değeri olduğu gibi başlığa yansıtmaktan çok daha güvenlidir.

Sık yapılan hatalar

Sık yapılan hatalar: Hiç başlık göndermemek., X-Frame-Options'ı meta etiketiyle vermek., ALLOW-FROM kullanmak., default-src'nin çerçevelemeyi de kapsadığını sanmak.
  • Hiç başlık göndermemek. MDN'e göre başlık ve başka bir mekanizma yoksa tarayıcı gömmeye izin verir.
  • X-Frame-Options'ı meta etiketiyle vermek. Etkisi yoktur; HTTP başlığı olarak gönderin.
  • ALLOW-FROM kullanmak. Eskimiştir ve modern tarayıcılar başlığı tamamen yok sayar.
  • default-src'nin çerçevelemeyi de kapsadığını sanmak. frame-ancestors ayrıca yazılmalıdır.
  • 'none' ve 'self' değerlerini tırnaksız yazmak. Tırnaklar zorunludur.
  • frame-src ile frame-ancestors'ı karıştırmak. Clickjacking koruması frame-ancestors'tır.
  • Başlığı yalnızca başarılı yanıtlara eklemek. Nginx'te always olmadan hata sayfaları başlıksız kalabilir.
  • İç içe çerçevelerde ara ataları unutmak. Zincirdeki her ata listede olmalıdır.

Kontrol listesi

  • Gömülmesi gerekmeyen tüm yanıtlara Content-Security-Policy: frame-ancestors 'none' ekleyin.
  • Eski tarayıcılar için ayrıca X-Frame-Options: DENY (veya aynı kökenden gömme gerekiyorsa SAMEORIGIN) gönderin.
  • Gömülmesi gereken sayfalarda yalnızca gerçekten güvendiğiniz kökenleri listeleyin.
  • Oturum çerezlerinde SameSite=Lax veya Strict kullanın.
  • Başlıkların gerçekten gönderildiğini tarayıcının geliştirici araçlarındaki ağ panelinde, yanıt başlıklarından kontrol edin.

Sık Sorulan Sorular

X-Frame-Options artık gerekli mi?

frame-ancestors onun yerine geçen ve daha esnek olan yönergedir. MDN ikisini birlikte göndermenin, frame-ancestors desteklemeyen tarayıcılarda da koruma sağladığını belirtir; destekleyen tarayıcılar ikisi birlikteyken X-Frame-Options'ı yok sayar.

Kendi sitemde iframe kullanıyorum, DENY bunu bozar mı?

DENY, aynı kökenden gömmeyi de engeller. Kendi sayfalarınızı kendi sitenizde çerçeveliyorsanız SAMEORIGIN ya da frame-ancestors 'self' kullanın. Bu başlıklar, çerçeve içinde gösterilen sayfanın yanıtına eklenir, çerçeveyi içeren sayfaya değil.

JavaScript ile çerçeveden çıkma kodu yeterli mi?

MDN'in clickjacking savunmaları arasında betik tabanlı bir çözüm yer almaz; önerilen yöntem gömülmeyi HTTP başlıklarıyla tarayıcı düzeyinde engellemektir. Başlıklar sayfa yüklenmeden önce uygulandığı için betiğin çalışmasına bağlı değildir.

Clickjacking ile CSRF arasındaki fark nedir?

CSRF'te saldırgan sahte isteği kendi sayfasından kurar ve tokenler bunu engeller. Clickjacking'de istek, gizlenmiş gerçek sayfadaki gerçek bir tıklamadan doğar; geçerli token de bu sayfanın içindedir. Bu yüzden ikisi farklı savunmalar gerektirir.

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ı · CSRF saldırısı · Set-Cookie öznitelikleri · postMessage ile iframe iletişimi · HTTPS ve TLS rehberi · Web API güvenlik hataları