Tarayıcının Güvenlik Modeli: Same-Origin Policy, Origin ile Site Farkı, SameSite Cookie'ler ve Fetch Metadata
Same-Origin Policy cross-origin isteği engellemez, yalnız yanıtın okunmasını engeller; isteğin yanında hangi cookie'nin gideceğine SameSite karar verir, sunucu da Sec-Fetch-* header'larına bakarak isteği reddedebilir. Bu yazıda origin ile site farkını, CORS'un SOP'yi neden gevşettiğini, Strict/Lax/None cookie'lerin ve Fetch Metadata header'larının headless Chrome'da gerçek davranışını ASP.NET Core 10 lab'ında logluyor ve bir Fetch Metadata policy'sini testleriyle kuruyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10, Chrome 156 ve Playwright 1.65 ile denenmiştir.
Önce Tahmin Et
Bookstore kullanıcısı api.bookstore.test'e giriş yapmış; tarayıcıda üç session cookie'si var: biri SameSite=Strict, biri SameSite=Lax, biri SameSite=None. Aynı tarayıcıda başka bir sekmede attacker.test adlı bir sayfa açılıyor ve bu sayfa API'ye (Application Programming Interface) fetch(..., { credentials: 'include' }) ile istek atıyor. CORS (Cross-Origin Resource Sharing) ayarı yok, tarayıcı konsolunda kırmızı bir hata çıkıyor. Soru şu: istek sunucuya ulaştı mı, ulaştıysa yanında hangi cookie'ler vardı? Peki aynı isteği shop.bookstore.test'ten atarsan ne değişir?
Çoğu ekip "CORS hatası verdi, demek ki istek gitmedi" der. Bu yazının sonunda bunun neden yanlış olduğunu kendi loglarımızda göreceğiz. Frontend Güvenliği serisinin ilk yazısında tarayıcının güvenlik modelini kuruyoruz: origin ile site farkı, Same-Origin Policy'nin gerçekte neyi engellediği, SameSite cookie'ler ve sunucunun karar vermek için kullanabileceği Fetch Metadata header'ları. Bütün denemeler kendi makinemdeki Bookstore lab'ına karşı; buradaki istekleri yalnız sahibi olduğun sistemlerde dene.
Origin ve Site: İki Ayrı Sınır
Tarayıcı iki farklı sınır çizer ve güvenlik mekanizmalarının yarısı birine, yarısı diğerine bakar. Origin, URL'nin (Uniform Resource Locator) üç parçasıdır: scheme, host ve port. https://shop.bookstore.test:19401 ile https://api.bookstore.test:19401 host'ları farklı olduğu için iki ayrı origin. Site ise daha kaba bir sınır: scheme artı registrable domain. Registrable domain, Public Suffix List (PSL) üzerinden hesaplanan eTLD+1'dir (effective top-level domain artı bir etiket). Bu iki adresin site'ı aynı: https://bookstore.test. Yani bunlar same-site ama cross-origin.
| A | B | Origin | Site |
|---|---|---|---|
https://shop.bookstore.test | https://api.bookstore.test | cross-origin | same-site |
https://shop.bookstore.test | https://shop.bookstore.test:8443 | cross-origin | same-site |
https://shop.bookstore.test | http://shop.bookstore.test | cross-origin | cross-site (schemeful) |
https://ada.github.io | https://mallory.github.io | cross-origin | cross-site (github.io PSL'de) |
https://shop.bookstore.test | https://attacker.test | cross-origin | cross-site |
İki satır dikkat istiyor. Port, origin'in parçası ama site'ın parçası değil. Scheme ise "schemeful same-site" ile site'a da girdi: Chrome 91'den beri http:// ve https:// aynı domain'de olsa bile cross-site sayılıyor; MDN'in (MDN Web Docs) uyumluluk tablosuna göre Firefox'ta bu davranış bir tercih bayrağının arkasında, Safari'de yok. github.io satırı da PSL'nin neden önemli olduğunu gösteriyor: barındırma platformları kendi suffix'lerini listeye ekletir, böylece iki farklı kullanıcının sayfası birbirine same-site görünmez.

Lab: Üç Host, Tek Süreç
Bunları tahminle değil logla konuşmak için tek bir ASP.NET Core 10 minimal API yazdım. Aynı Kestrel süreci üç host adına cevap veriyor: api.bookstore.test (API ve login), shop.bookstore.test (Bookstore'un mağaza sayfası) ve attacker.test (başka bir site). Fetch Metadata header'ları yalnız potentially trustworthy URL'lere gönderildiği için lab HTTPS (Hypertext Transfer Protocol Secure) üzerinde çalışıyor; sertifikayı çalışma klasöründe openssl ile ürettim, dotnet dev-certs kullanmadım. Program.cs:
using Bookstore.Lab;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => options.AddPolicy("shop", policy => policy
.WithOrigins("https://shop.bookstore.test:19401")
.AllowCredentials()));
var app = builder.Build();
ILogger log = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Bookstore.Lab");
bool fetchMetadataEnabled = app.Configuration.GetValue<bool>("FetchMetadata:Enabled");
// Her API isteğinde tarayıcının gönderdiği Fetch Metadata header'larını ve cookie adlarını logla
app.Use(async (context, next) =>
{
HttpRequest request = context.Request;
if (request.Path.StartsWithSegments("/api"))
{
string cookies = string.Join(",", request.Cookies.Keys.Order());
log.LogInformation("{Method} {Host}{Path} site={Site} mode={Mode} dest={Dest} origin={Origin} cookies=[{Cookies}]",
request.Method, request.Host, request.Path,
request.Headers["Sec-Fetch-Site"], request.Headers["Sec-Fetch-Mode"], request.Headers["Sec-Fetch-Dest"],
request.Headers.Origin, cookies);
if (fetchMetadataEnabled && !FetchMetadataPolicy.IsAllowed(request))
{
log.LogWarning("Blocked by Fetch Metadata policy: {Method} {Path}", request.Method, request.Path);
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
}
await next();
});
app.UseCors();
// api.bookstore.test: oturum açınca üç farklı SameSite değeriyle cookie bırak
app.MapGet("/login", (HttpContext context) =>
{
CookieOptions secure = new() { Secure = true, HttpOnly = true, Path = "/" };
context.Response.Cookies.Append("sid_strict", "s1", new CookieOptions(secure) { SameSite = SameSiteMode.Strict });
context.Response.Cookies.Append("sid_lax", "s2", new CookieOptions(secure) { SameSite = SameSiteMode.Lax });
context.Response.Cookies.Append("sid_none", "s3", new CookieOptions(secure) { SameSite = SameSiteMode.None });
return Results.Text("logged in");
}).RequireHost("api.bookstore.test");
app.MapGet("/api/whoami", (HttpRequest request) =>
Results.Ok(new { user = request.Cookies.ContainsKey("sid_strict") ? "ada" : "anonymous" }))
.RequireCors("shop");
app.MapPost("/api/cart/items", (HttpRequest request) =>
Results.Ok(new { added = true, cookies = request.Cookies.Count }));
// Aynı süreç, iki farklı "site" için test sayfası sunar
app.MapGet("/", () => Results.Content(Pages.Probe, "text/html"))
.RequireHost("shop.bookstore.test", "attacker.test");
app.Run();
public partial class Program;
Login, aynı değerlere sahip ama SameSite'ı farklı üç cookie bırakıyor; hepsi Secure ve HttpOnly. İlk middleware /api altındaki her isteğin method'unu, Fetch Metadata header'larını, Origin'i ve gelen cookie adlarını logluyor. Kestrel endpoint'i appsettings.json'da https://127.0.0.1:19401, sertifika yolu ../certs/lab.crt. Diğer iki host aynı HTML (HyperText Markup Language) sayfasını sunuyor:
namespace Bookstore.Lab;
public static class Pages
{
public const string Api = "https://api.bookstore.test:19401";
public const string Probe = $$"""
<!doctype html>
<html>
<body>
<a id="nav" href="{{Api}}/api/whoami">whoami</a>
<form id="cart" method="post" action="{{Api}}/api/cart/items">
<input name="bookId" value="42">
</form>
<img id="pixel" alt="">
</body>
</html>
""";
}
Tarayıcı tarafını headless Chrome 156 ile Playwright üzerinden sürdüm. --host-resolver-rules bayrağı üç host adını da 127.0.0.1'e çeviriyor; böylece hosts dosyasına ya da DNS'e (Domain Name System) dokunmadan gerçek cross-site ve same-site istekleri elde ediyoruz:
import { chromium } from 'playwright-core';
const API = 'https://api.bookstore.test:19401';
const browser = await chromium.launch({
executablePath: process.env.CHROME,
headless: true,
args: ['--host-resolver-rules=MAP *.bookstore.test 127.0.0.1, MAP attacker.test 127.0.0.1'],
});
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
page.on('console', m => console.log(`[console.${m.type()}] ${m.text()}`));
Script önce /login'e gidiyor, sonra her iki sayfadan sırayla aynı beş isteği yapıyor: credential'lı fetch ile GET, no-cors POST, bir <img>, link tıklaması ve form submit. İlk iki adım şöyle:
for (const origin of ['https://attacker.test:19401', 'https://shop.bookstore.test:19401']) {
step(`${origin}: fetch GET with credentials`);
await page.goto(`${origin}/`);
console.log(await page.evaluate(async api => {
try {
const r = await fetch(`${api}/api/whoami`, { credentials: 'include' });
return `read ${r.status} ${await r.text()}`;
} catch (e) { return `${e.name}: ${e.message}`; }
}, API));
step(`${origin}: fetch POST no-cors with credentials`);
console.log(await page.evaluate(async api => {
const r = await fetch(`${api}/api/cart/items`, { method: 'POST', mode: 'no-cors', credentials: 'include', body: 'bookId=42' });
cd Bookstore.Lab
FetchMetadata__Enabled=false dotnet run
# ikinci terminalde
node probe01.mjs
Same-Origin Policy Okumayı Engeller, Göndermeyi Değil
attacker.test'teki fetch'in tarayıcıdaki sonucu beklediğin gibi:
### https://attacker.test:19401: fetch GET with credentials
[console.error] Access to fetch at 'https://api.bookstore.test:19401/api/whoami' from origin 'https://attacker.test:19401' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
[console.error] Failed to load resource: net::ERR_FAILED
TypeError: Failed to fetch
Şimdi aynı anda sunucunun loguna bakalım. Policy'yi kapattığım ilk koşuda attacker.test'ten gelen beş isteğin beşi de sunucuya ulaştı:
GET api.bookstore.test:19401/api/whoami site=cross-site mode=cors dest=empty origin=https://attacker.test:19401 cookies=[sid_none]
POST api.bookstore.test:19401/api/cart/items site=cross-site mode=no-cors dest=empty origin=https://attacker.test:19401 cookies=[sid_none]
GET api.bookstore.test:19401/api/whoami site=cross-site mode=no-cors dest=image origin= cookies=[sid_none]
GET api.bookstore.test:19401/api/whoami site=cross-site mode=navigate dest=document origin= cookies=[sid_lax,sid_none]
POST api.bookstore.test:19401/api/cart/items site=cross-site mode=navigate dest=document origin=https://attacker.test:19401 cookies=[sid_none]
Same-Origin Policy (SOP), bir origin'deki script'in başka bir origin'in yanıtını okumasını engeller. İsteğin gönderilmesini engellemez. İlk satırdaki fetch sunucuya gitti, sunucu 200 döndü; tarayıcı yanıtta Access-Control-Allow-Origin görmediği için gövdeyi JavaScript'e vermedi. no-cors POST'un ve form submit'in sonucu sunucu tarafında zaten gerçekleşmiş bir yan etki: sepete kitap eklendi. CSRF (Cross-Site Request Forgery) tam olarak bu boşluğu kullanır; cookie flag'leri ve CSRF token'ı ile temel savunmayı OWASP (Open Worldwide Application Security Project) serisinin "Authentication Failures (2)" yazısında konuştuk.
CORS bu tabloda nerede duruyor? CORS, SOP'yi gevşeten bir mekanizma, sıkılaştıran değil. Sunucu "bu origin yanıtı okuyabilir" der, o kadar. Lab'da yalnız shop.bookstore.test'e izin verdim:
curl -sk -i --resolve api.bookstore.test:19401:127.0.0.1 -H "Origin: https://shop.bookstore.test:19401" https://api.bookstore.test:19401/api/whoami
HTTP/2 200
content-type: application/json; charset=utf-8
date: Sun, 08 Nov 2026 05:24:58 GMT
server: Kestrel
access-control-allow-credentials: true
access-control-allow-origin: https://shop.bookstore.test:19401
{"user":"anonymous"}
### https://shop.bookstore.test:19401: fetch GET with credentials
read 200 {"user":"ada"}
Aynı fetch, mağaza sayfasından çalışınca yanıtı okuyabildi. attacker.test için header hiç yazılmadı. Ama iki durumda da istek sunucuya ulaştı ve sunucu işini yaptı. "CORS'u kısıtladık, CSRF'e karşı korunuyoruz" cümlesi bu yüzden yanlış. CORS'un sunucu tarafındaki ayarlarını ASP.NET Core Web API serisinin "CORS ve Güvenlik" yazısında ve OWASP serisinin "Broken Access Control (2)" yazısında anlattık.
SameSite Cookie'ler: Strict, Lax ve None
Yukarıdaki logun cookies= sütununa tekrar bak. attacker.test'ten gelen isteklerde:
| İstek | Sec-Fetch-Mode / Dest | Gelen cookie'ler |
|---|---|---|
fetch GET | cors / empty | sid_none |
fetch POST | no-cors / empty | sid_none |
<img> | no-cors / image | sid_none |
| Link tıklaması (GET) | navigate / document | sid_lax, sid_none |
| Form submit (POST) | navigate / document | sid_none |
Strict cookie, cross-site hiçbir isteğe eklenmedi; link tıklamasında bile. Bu yüzden link tıklamasıyla gelen /api/whoami kullanıcıyı anonymous gördü. Lax cookie yalnız güvenli method'la (GET) yapılan top-level navigasyonda gitti; cross-site form POST'unda gitmedi. None her yere gitti; bu cookie için tek koruma, sunucunun kendi CSRF kontrolü.
SameSite yazılmamış bir cookie'nin ne olacağı tarayıcıya göre değişiyor. Chrome 80'den beri böyle bir cookie'yi Lax sayıyor. MDN'in uyumluluk verisine göre Firefox'ta bu varsayılan bir tercih bayrağının arkasında, Safari'de hiç yok. Yani SameSite'ı her zaman açıkça yaz; varsayılana güvenmek, Bookstore'un Safari kullanıcılarını korumasız bırakmak demek. SameSite=None için Secure zorunlu; Chrome bunu 80'den, Firefox 131'den beri uyguluyor.
Same-Site Ama Cross-Origin: Subdomain Riski
Aynı beş isteği shop.bookstore.test'ten yaptığımızda log şöyle:
GET api.bookstore.test:19401/api/whoami site=same-site mode=cors dest=empty origin=https://shop.bookstore.test:19401 cookies=[sid_lax,sid_none,sid_strict]
POST api.bookstore.test:19401/api/cart/items site=same-site mode=no-cors dest=empty origin=https://shop.bookstore.test:19401 cookies=[sid_lax,sid_none,sid_strict]
GET api.bookstore.test:19401/api/whoami site=same-site mode=no-cors dest=image origin= cookies=[sid_lax,sid_none,sid_strict]
GET api.bookstore.test:19401/api/whoami site=same-site mode=navigate dest=document origin= cookies=[sid_lax,sid_none,sid_strict]
POST api.bookstore.test:19401/api/cart/items site=same-site mode=navigate dest=document origin=https://shop.bookstore.test:19401 cookies=[sid_lax,sid_none,sid_strict]
Beş isteğin hepsinde üç cookie de gitti, Strict dahil. Çünkü SameSite'ın sınırı origin değil site. shop ile api aynı site'ta olduğu sürece tarayıcının gözünde bu istekler "kendi sitenden" geliyor. Sonucu düşün: bookstore.test altında pazarlama ekibinin yönettiği bir blog.bookstore.test varsa ve orada bir XSS (Cross-Site Scripting) açığı çıkarsa, oradaki script api.bookstore.test'e Strict cookie'lerle istek atabilir. SameSite subdomain'leri birbirinden korumaz. Aynı registrable domain altında üçüncü parti içerik barındırıyorsan (kullanıcı sayfaları, eski bir WordPress, bir test ortamı) ya onu ayrı bir site'a taşı ya da API'de origin seviyesinde kontrol yap. Cookie'yi Domain attribute'u olmadan, __Host- prefix'iyle bırakmak da yardımcı olur: o cookie yalnız onu bırakan host'a gider ve bir subdomain tarafından üzerine yazılamaz.
Fetch Metadata: Sunucu Kararı Header'a Bakarak Verir
Logda her satırda gördüğün Sec-Fetch-Site, Sec-Fetch-Mode ve Sec-Fetch-Dest, W3C'nin (World Wide Web Consortium) Fetch Metadata Request Headers taslağında tanımlı. Sec- ile başladıkları için forbidden header'lar: sayfadaki JavaScript bunları ne değiştirebilir ne de taklit edebilir. Değerler isteğin kim tarafından ve nasıl başlatıldığını söylüyor: Sec-Fetch-Site için same-origin, same-site, cross-site ve kullanıcının adres çubuğundan ya da yer iminden gelen istekler için none. MDN'e göre header Chrome 76, Firefox 90 ve Safari 16.4'ten beri destekleniyor.
Bu bilgiyle sunucu, isteği işlemeden önce karar verebilir. web.dev'in "Resource Isolation Policy" önerisini Bookstore'a uyarladım:
namespace Bookstore.Lab;
public static class FetchMetadataPolicy
{
// Resource Isolation Policy: https://web.dev/articles/fetch-metadata
public static bool IsAllowed(HttpRequest request)
{
string site = request.Headers["Sec-Fetch-Site"].ToString();
// Header yoksa: eski tarayıcı ya da tarayıcı dışı istemci (curl, servis)
if (site is "" or "same-origin" or "same-site" or "none")
{
return true;
}
// cross-site: yalnız GET ile yapılan basit top-level navigasyona izin ver
string mode = request.Headers["Sec-Fetch-Mode"].ToString();
string dest = request.Headers["Sec-Fetch-Dest"].ToString();
return mode == "navigate"
&& HttpMethods.IsGet(request.Method)
&& dest is not ("object" or "embed");
}
}
Kural basit: header yoksa izin ver (tarayıcı dışı istemciler ve eski tarayıcılar kırılmasın), same-origin, same-site ve none isteklere izin ver, cross-site isteklerde yalnız GET ile yapılan top-level navigasyonu kabul et. object ve embed hedefleri navigasyon gibi görünse de başka bir sayfanın içine gömülüyor, onları dışarıda bırakıyoruz. Policy açıkken aynı probe'un logu:
info: Bookstore.Lab[0] GET api.bookstore.test:19401/api/whoami site=cross-site mode=cors dest=empty origin=https://attacker.test:19401 cookies=[sid_none]
warn: Bookstore.Lab[0] Blocked by Fetch Metadata policy: GET /api/whoami
info: Bookstore.Lab[0] POST api.bookstore.test:19401/api/cart/items site=cross-site mode=no-cors dest=empty origin=https://attacker.test:19401 cookies=[sid_none]
warn: Bookstore.Lab[0] Blocked by Fetch Metadata policy: POST /api/cart/items
info: Bookstore.Lab[0] GET api.bookstore.test:19401/api/whoami site=cross-site mode=no-cors dest=image origin= cookies=[sid_none]
warn: Bookstore.Lab[0] Blocked by Fetch Metadata policy: GET /api/whoami
info: Bookstore.Lab[0] POST api.bookstore.test:19401/api/cart/items site=cross-site mode=navigate dest=document origin=https://attacker.test:19401 cookies=[sid_none]
warn: Bookstore.Lab[0] Blocked by Fetch Metadata policy: POST /api/cart/items
Cross-site gelen dört istek endpoint koduna ulaşmadan 403 aldı; form submit artık sepete kitap eklemiyor. Link tıklaması hâlâ çalışıyor, çünkü başka bir siteden Bookstore'a link vermek meşru bir durum. Bu kararı otomatik testle sabitledim; testler WebApplicationFactory ile uygulamayı bellekte ayağa kaldırıyor:
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Bookstore.Lab.Tests;
public class FetchMetadataTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Theory]
[InlineData("POST", "/api/cart/items", "cross-site", "no-cors", "empty", HttpStatusCode.Forbidden)]
[InlineData("POST", "/api/cart/items", "cross-site", "navigate", "document", HttpStatusCode.Forbidden)]
[InlineData("GET", "/api/whoami", "cross-site", "no-cors", "image", HttpStatusCode.Forbidden)]
[InlineData("GET", "/api/whoami", "cross-site", "navigate", "document", HttpStatusCode.OK)]
[InlineData("POST", "/api/cart/items", "same-site", "cors", "empty", HttpStatusCode.OK)]
[InlineData("POST", "/api/cart/items", "same-origin", "cors", "empty", HttpStatusCode.OK)]
public async Task Policy_decides_by_fetch_metadata(
string method, string path, string site, string mode, string dest, HttpStatusCode expected)
{
HttpClient client = factory.CreateClient();
HttpRequestMessage request = new(new HttpMethod(method), path);
request.Headers.Add("Sec-Fetch-Site", site);
request.Headers.Add("Sec-Fetch-Mode", mode);
request.Headers.Add("Sec-Fetch-Dest", dest);
HttpResponseMessage response = await client.SendAsync(request);
Assert.Equal(expected, response.StatusCode);
}
[Fact]
public async Task Requests_without_fetch_metadata_are_allowed()
{
// curl ya da Sec-Fetch-* göndermeyen eski tarayıcı
HttpClient client = factory.CreateClient();
HttpResponseMessage response = await client.PostAsync("/api/cart/items", null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
dotnet test
Passed! - Failed: 0, Passed: 7, Skipped: 0, Total: 7, Duration: 126 ms - Bookstore.Lab.Tests.dll (net10.0)
Aynı fikir framework'e de giriyor: .NET 11'in preview sürümlerinde ASP.NET Core, form okuyan endpoint'ler için Sec-Fetch-Site ve Origin'e bakan, varsayılan olarak açık bir CSRF koruması getiriyor. .NET 10'da bu yok; yukarıdaki middleware ya da antiforgery token'ı senin işin.
2026'da Tarayıcı Durumu: Third-Party Cookie'ler, CHIPS ve FedCM
Birkaç yıl boyunca "Chrome third-party cookie'leri kapatacak, CSRF kendiliğinden azalacak" diye plan yapan ekipler oldu. Bu plan gerçekleşmedi. Google Nisan 2025'te Chrome'daki third-party cookie seçiminde mevcut yaklaşımı koruyacağını, yani bu cookie'lerin varsayılan olarak açık kalacağını, 17 Ekim 2025'teki Privacy Sandbox güncellemesinde de Topics, Protected Audience, Attribution Reporting, IP (Internet Protocol) Protection ve Related Website Sets dahil API'lerin çoğunu emekliye ayıracağını duyurdu. Desteklenmeye devam edenler arasında iki tanesi bizi doğrudan ilgilendiriyor: CHIPS (Cookies Having Independent Partitioned State) ve FedCM (Federated Credential Management).
CHIPS, Partitioned attribute'u ile cookie'yi top-level site'a göre bölmeye yarar: bir ödeme widget'ı Bookstore içinde iframe olarak çalışırken bıraktığı cookie, aynı widget başka bir sitede açıldığında görünmez. MDN'e göre Chrome 114, Firefox 141 ve Safari 26.2 destekliyor. FedCM ise third-party cookie'ye dayanmadan kimlik sağlayıcıyla oturum açma akışı sunan bir tarayıcı API'si. Bookstore için pratik sonuç: third-party cookie'nin kalkmasını bir güvenlik önlemi gibi hesaba katma. SameSite'ı kendin yaz, cross-site gelen isteği sunucuda kendin reddet.
Üretimde Dikkat
Fetch Metadata policy'sini doğrudan enforce ederek açma. Önce yalnız logla: lab'daki log satırını kendi uygulamanda bir hafta tut, site=cross-site olup policy'ye takılacak istekleri say. Ödeme sağlayıcısının callback'i, partner sitelerden gelen form POST'ları ya da bir mobil uygulamanın WebView'ı bu listede çıkabilir. Bunları endpoint bazında istisna olarak ekle, sonra enforce'a geç.
Header'ın yokluğu bir kanıt değil. Sec-Fetch-* göndermeyen eski tarayıcılar ve HTTP (Hypertext Transfer Protocol) üzerinden çalışan sayfalar policy'den geçer. Bu yüzden Fetch Metadata, SameSite cookie ve CSRF token'ın yerine geçmez; onların üstüne eklenen ucuz bir katmandır. Reverse proxy ya da CDN (Content Delivery Network) Sec-Fetch-* header'larını silmemeli; bunu staging ortamında bir istekle kontrol et. Son olarak, policy'nin döndürdüğü 403'leri ayrı bir metrikle say: ani bir artış ya bir saldırı denemesini ya da senin kırdığın meşru bir akışı gösterir, ikisini de bilmek istersin.
Sık Düşülen Tuzaklar
Birincisi, konsolda CORS hatası görünce isteğin gitmediğini sanmak. Lab'ın ilk satırı bunun tersini gösteriyor: sunucu isteği aldı, işledi, 200 döndü. Durum değiştiren her endpoint'i, yanıtı kimse okuyamasa bile çalışacağını varsayarak koru.
İkincisi, state değiştiren işlemleri GET ile yapmak. Lax cookie cross-site link tıklamasında gidiyor; GET /api/cart/clear gibi bir endpoint, başka bir sitedeki tek bir link ile tetiklenebilir.
Üçüncüsü, SameSite'ı yazmayıp tarayıcı varsayılanına güvenmek. Chrome'daki Lax varsayılanı Safari'de yok.
Dördüncüsü, subdomain'leri güvenli sınır sanmak. shop ile blog aynı site; birindeki XSS, diğerine Strict cookie'lerle istek atabilir.
Beşincisi, Fetch Metadata policy'sinde header yokken isteği reddetmek. curl ile çalışan entegrasyon testlerin, health check'lerin ve sunucudan sunucuya çağrıların bir gecede kırılır.
Küçük Bir Deneme
★☆☆ Lab'da sid_lax cookie'sinin SameSite değerini kaldır, probe'u tekrar çalıştır ve attacker.test'ten gelen link tıklamasında hangi cookie'lerin gittiğini logdan oku. Sonucu Chrome'un Lax varsayılanıyla karşılaştır.
★★☆ FetchMetadataPolicy'yi same-site istekleri de reddedecek şekilde sıkılaştır, sonra shop.bookstore.test'ten gelen fetch'in hâlâ çalışması için hangi endpoint'lere istisna gerektiğini bul. Testlere bu durumu kapsayan bir satır ekle.
★★★ Policy'yi önce "yalnız logla" modunda çalışacak hâle getir: FetchMetadata:Mode ayarı Report iken isteği engellemesin, yalnız uyarı loglasın. İki mod için de test yaz.
Bir sonraki yazıda tarayıcının içine, Angular uygulamasının kendisine dönüyoruz: Angular'ın sanitization'ının korumadığı yollar. ElementRef, Renderer2, dinamik URL'ler ve HTML üreten kütüphanelerle bir DOM (Document Object Model) XSS sink envanteri ve code review listesi çıkaracağız.