İçeriğe geç
LexerLangv0.2.0

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:

LexerLang
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 with

Güvenlik başlıkları#

LexerLang
security.headers({
    content_security_policy: "default-src 'self'",
    frame_options: "SAMEORIGIN",
    hsts_days: 180
})

Varsayılan olarak yazılanlar:

Metin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin

CSP 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#

LexerLang
$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:

LexerLang
$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 proxy

nginx'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:

Metin
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ı#

LexerLang
$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:

LexerLang
$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:

LexerLang
$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 false dönerse 403 gider.
  • true döner ya da hiçbir şey yazmazsa istek sürer.
  • paths yalnızca o öneklerde çalıştırır; skip dış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#

LexerLang
$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ı.

LexerLang
# Formda
$doc.body.add(`
    <form method="POST" action="/gonder">
        ` + security.csrf.field() + `
        <button>Gönder</button>
    </form>
`)
LexerLang
# İşleyicide
if not security.csrf.check($req) then
    $res.status(403)
    $res.send("geçersiz istek")
    return
end if

check ö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.

LexerLang
$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:

LexerLang
$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.

LexerLang
$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.

Sırada#

Üretim.