Ara katmanlar
security_headers, use_cors, rate_limit, trust_proxy, before ve CSRF.
Uygulama seviyesinde bir kez yazılır, her isteğe uygulanır:
with $app
.trust_proxy(true)
.security_headers({content_security_policy: "default-src 'self'"})
.use_cors({origins: ["https://ornek.com"], credentials: true})
.rate_limit({limit: 60, window: 60, redis: $redis})
end withGüvenlik başlıkları#
security.headers({
content_security_policy: "default-src 'self'",
frame_options: "SAMEORIGIN",
hsts_days: 180
})Varsayılan olarak yazılanlar:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-originCSP ve HSTS istenmeden yazılmaz. Doğru CSP uygulamaya göre değişir; uydurma bir değer ya sayfayı bozar ya da hiçbir şey korumaz. HSTS ise HTTPS'e geçmemiş bir siteyi tarayıcı tarafında erişilemez kılar — ve geri alınması sürenin dolmasını beklemeyi gerektirir.
trust_proxy#
$app.trust_proxy(true)$req.ip varsayılan olarak proxy başlığını (X-Forwarded-For) okumaz. Okusaydı istemci kendi adresini uydurabilir ve IP'ye dayanan her sınır — hız sınırı dahil — anlamsız hale gelirdi.
trust_proxy(true) yalnızca gerçekten bir ters proxy arkasındaysanız açılmalıdır. Açıkken bile başlığa yalnızca bağlantı güvenilen bir proxy'den geliyorsa bakılır ve zincir sağdan okunur:
$app.trust_proxy(true) # aynı makinedeki nginx (127.0.0.1, ::1)
$app.trust_proxy({proxies: ["127.0.0.1", "10.0.0.0/8"]}) # bu adreslerden gelen bağlantılar
$app.trust_proxy({hops: 1}) # önde tam olarak bir proxynginx'in varsayılanı ($proxy_add_x_forwarded_for) gelen zincirin sonuna gerçek adresi ekler; en soldaki adresi istemci kendisi yazabilir. Bu yüzden güvenilen proxy adresleri sağdan atlanır ve kalan ilk adres istemci olur:
bağlantı 127.0.0.1, X-Forwarded-For: 6.6.6.6, 203.0.113.9 → $req.ip = 203.0.113.9
bağlantı 198.51.100.7, X-Forwarded-For: 203.0.113.9 → $req.ip = 198.51.100.7$req.forwarded_ip zincirin en solunu ham olarak verir; ona güvenmeyin.
Hız sınırı#
$app.rate_limit({limit: 60, window: 60, redis: $redis})Dakikada 60 istek. Sınır route'tan önce çalışır; aşılmışsa işleyici hiç çalışmaz.
Sayaç IP başınadır. redis verilmezse sayaç bellekte tutulur — tek süreçte çalışan bir uygulama için yeterli, birden fazla süreç varsa her biri kendi sayacını tutar ve gerçek sınır kat kat yükselir.
Redis anahtarı lexer:rate:<name>:<ip> biçimindedir. Redis ACL ile uygulama başına anahtar deseni kısıtlıyorsanız öneki değiştirin:
$app.rate_limit({name: "giris", limit: 20, window: 300, prefix: "magaza:rate:", redis: $redis})before — isteği kesmek#
Kimlik ya da IP kontrolünü her route'a ayrı ayrı yazmak, birinde unutulduğu gün bir açıktır. before her istekte, statik dosyalardan ve route'lardan önce çalışır:
$app.before(func($req, $res) as bool
if not izinli($req.ip) then
$res.status(403).send("yasak")
return false
end if
return true
end func, {skip: ["/public/"]})
$app.before(giris_gerekli, {paths: ["/admin/"], skip: ["/admin/giris"]})- Yanıt yazarsa (
send,redirect) istek orada biter; yanıt odur. - Yanıt yazmadan
falsedönerse403gider. truedöner ya da hiçbir şey yazmazsa istek sürer.pathsyalnızca o öneklerde çalıştırır;skipdışarıda bırakır.
Sıra: CORS ön kontrolü ve hız sınırı → before → statik dosya → route. Statik dosyaların arkada kalması bilerek: IP izin listesi /public/ altını da korur.
CORS#
$app.use_cors({origins: ["https://ornek.com"], credentials: true})credentials: true ile origins: ["*"] birlikte kullanılamaz; tarayıcılar bu bileşimi zaten reddeder. Kimlik taşıyan isteklerde izin verilen kaynakları tek tek yazmanız gerekir.
CSRF#
Token oturumda durur ve oturum boyunca değişmez — her istekte yenilenseydi aynı anda açık iki sekme birbirinin tokenini geçersiz kılardı.
# Formda
$doc.body.add(`
<form method="POST" action="/gonder">
` + security.csrf.field() + `
<button>Gönder</button>
</form>
`)# İşleyicide
if not security.csrf.check($req) then
$res.status(403)
$res.send("geçersiz istek")
return
end ifcheck önce _csrf form alanına, sonra X-CSRF-Token başlığına bakar: form gönderimleri ilkini, tarayıcıdan atılan istekler ikincisini kullanır. Karşılaştırma sabit süredir.
| Çağrı | Nereye |
|---|---|
security.csrf.token() | ham değer |
security.csrf.field() | form içine gizli alan |
security.csrf.meta() | belge başlığına meta etiketi |
security.csrf.check($req) | doğrulama |
Zengin metin#
sanitize_html yalnızca gerçekten HTML saklamak zorunda olduğunuz yer içindir — bir zengin metin editörünün çıktısı gibi.
$doc.body.add(security.sanitize_html($yazi.govde))İzin listesiyle çalışır: p b strong em i u s a img ul ol li h1-h4 code pre blockquote hr br. Bunun dışındaki her etiket metne döner, her öznitelik düşer. script ve style içerikleriyle birlikte silinir. href/src yalnızca http, https, mailto, tel ve göreceli adresleri kabul eder — javascript: ve data: birer kod çalıştırma yoludur.
SQL tarafı#
SQL için kaçışlama işlevi yok: değerler zaten parametre olarak gidiyor. Parametrelerin kapsamadığı tek yer tablo ve sütun adlarıdır:
$sutun = security.identifier($req.get("sirala"), ["ad", "fiyat", "stok"])
$db.query("SELECT * FROM urunler ORDER BY " + $sutun)identifier kaçışlamaz, izin listesinden seçer. Eşleşme yoksa listenin ilk öğesi döner; çağıran her durumda geçerli bir ad alır.
$db.query("SELECT * FROM urunler WHERE ad LIKE ?",
"%" + security.escape_like($arama) + "%")escape_like, % ve _ karakterlerini kaçışlar. Bu bir enjeksiyon değil ama kullanıcının yazdığı % bütün kayıtları getirirdi.