Geri

Jaga'yı neden yazdım: XSS'in kenarda kalan yerleri

Doğukan Batal

Bu yazıda
  1. 1. Ham HTML (örneğin React’teki dangerouslySetInnerHTML)
  2. 2. Attribute’lar
  3. 3. CSS injection
  4. 4. Sunucu tarafında render
  5. 5. Script bağlamı ve JSON
  6. Güvenlik bağlama bağlı
  7. Jaga’yı denemek

React ve Vue seni XSS saldırılarının çoğundan varsayılan olarak korur. Değerleri kendileri escape eder, sık yapılan hataları yapmayı zorlaştırırlar.

Geriye kalan sorunlar uçlarda birikir. Uç dediğim, o varsayılan korumanın dışına adım attığın yerler:

  • ham HTML basmak
  • dinamik attribute vermek
  • kullanıcıya özel stil izni tanımak
  • script’in içine veri gömmek

Gerçek projelerde bunlara tekrar tekrar takıldım ve sonunda hepsi için küçük bir kütüphane yazdım: Jaga (yeni sekmede açılır). Bu yazıda her ucu sırayla ele alıyorum: orada ne ters gidiyor, daha güvenli kural ne, Jaga bu kuralı nasıl uyguluyor.

Jaga logosu ve kapak görseli: HTML şablonları için hafif bir güvenlik katmanı

Jaga, HTML şablonları için bağımlılığı olmayan, çok hafif bir güvenlik katmanı. Kullanıcı girdisiyle DOM arasında durur ve XSS’e karşı bağlama göre koruma sağlar.

1. Ham HTML (örneğin React’teki dangerouslySetInnerHTML)

Bazen gerçekten HTML basman gerekir:

<div dangerouslySetInnerHTML={{ __html: content }} />

content temizlenmemişse kod çalıştırabilir:

<img src="x" onerror="alert(1)">

Akla gelen çözümlerin hepsinin bir bedeli var. Her şeyi escape edersen geçerli HTML de bozulur. Regex ile filtrelemek güvensizdir ve bazı durumları kaçırır. DOM tabanlı sanitizer’lar daha güvenli ama genelde ağırdır, sunucu tarafında daha da ağır.

Daha iyi kural, HTML’i yapısına bakarak temizlemek. script gibi tehlikeli etiketleri çıkarırsın, onerror ve onclick gibi güvensiz attribute’ları atarsın, güvenli işaretlemeye dokunmazsın.

Jaga bunu şablon seviyesinde yapıyor. HTML’i bir string gibi görmüyor; render etmeden önce parse edip filtreliyor. Güvensiz düğümler çıktıya hiç ulaşmıyor.

2. Attribute’lar

Escape etmek attribute’ları korumaz:

<a href={userInput}>Click</a>

userInput şuysa:

javascript:alert(1)

tarayıcı bunu çalıştırır. Escape karakterlerle ilgilenir, buradaki sorun ise değerin ne anlama geldiği. javascript:alert(1) içinde escape edilecek tek bir karakter yok, ama yine de bir script.

Güvenli kural şu: href’in bir URL olduğunu bil, yalnızca https ve mailto gibi güvenli protokollere izin ver, gerisini engelle.

Jaga href’in URL bağlamı olduğunu tanıyor. Değeri inceliyor, normalize ediyor ve javascript: gibi tehlikeli protokolleri, gizlenmiş hallerini de dahil ederek render’dan önce engelliyor.

3. CSS injection

CSS’in de bir güvenlik riski olduğu kolayca unutuluyor:

<div style={userStyle}></div>

Kontrol edilmezse saldırgan şunu gömebilir:

background: url("javascript:alert(1)");

ya da gizlenmiş bir değer:

color: \6a avascript:alert(1);

CSS’i filtrelemek zor. Kendi sözdizimi ve parse kuralları var, url() gibi fonksiyonlara izin veriyor, yukarıdaki gibi escape numaralarını destekliyor.

Sağlam kural, CSS’i düzgünce parse etmek, color ve margin gibi güvenli bir özellik kümesine izin vermek, tehlikeli değerleri ve fonksiyonları engellemek.

Jaga’da bunun için küçük bir sözcüksel CSS sanitizer var. Stil string’ini token’lara ayırıyor ve yalnızca güvenli özellik ve değerlerin geçmesine izin veriyor. Beklenen kalıplara uymayan her şey reddediliyor.

4. Sunucu tarafında render

Artık birçok uygulama sunucuda render ediliyor (Next.js ve benzerleri), sanitizer’ların çoğu ise tarayıcıdaki DOM’a dayanıyor. Sunucuda bu, jsdom gibi ek bağımlılıklar, daha fazla bellek ve daha yavaş çalışma demek.

Burada DOM gerektirmeyen, tarayıcıda da sunucuda da aynı davranan ve küçük kalan bir araç işini kolaylaştırır.

Jaga DOM API’lerini kullanmıyor; Node, Bun, Deno ve tarayıcıda aynı şekilde çalışıyor. Hiç bağımlılığı yok, yani projeni şişirmiyor ve tedarik zinciri riski eklemiyor. Bir de HTML’deki gereksiz boşlukları temizleyen bir minifier’ı var; <pre> ya da <textarea> içindeki boşluklara dokunmuyor. SSR’da küçük bir hız kazancı sağlıyor.

5. Script bağlamı ve JSON

Script’in içine veri gömmek de ters gidebilir:

<script>
  window.__STATE__ = ${data}
</script>

data şunu içeriyorsa:

"</script><script>alert(1)</script>"

script erken kapanır ve kendi kodunu çalıştırır. Bunu önlemek için JSON’ı güvenli biçimde encode etmek ve </script> gibi dizileri escape etmek gerekiyor.

Jaga’da bunun için j.json() yardımcısı var. Veriyi serialize ediyor ve script’ten taşabilecek her şeyi escape ediyor, böylece veri script’in içinde kalıyor. Jaga Trusted Types’ı da destekliyor: modern tarayıcıların güvendiği nesneler döndürebiliyor. Sıkı bir Content Security Policy (CSP) uygularken ekstra iş çıkarmıyor.

Güvenlik bağlama bağlı

Bütün örneklerde aynı şey tekrar ediyor: her yer kendi kuralını istiyor.

Bağlam Risk Kural Jaga’da
Ham HTML script çalışması yapıya göre temizle güvensiz etiket ve attribute’ları atan yapısal sanitizer
Attribute’lar (href, src) javascript: URL’leri yalnızca güvenli protokoller URL bağlamını tanır, güvensiz protokolü engeller
CSS zararlı stiller, gizli injection parse et, bilinen özellik ve değerlere izin ver sözcüksel CSS sanitizer
Script / JSON </script> ile dışarı taşma JSON’ı güvenli encode et j.json()
SSR ağır bağımlılıklar, DOM şartı DOM’suz araçlar sıfır bağımlılık; Node, Bun, Deno ve tarayıcıda çalışır

Her durumu elle de çözebilirsin. Ama bağlamı bilen tek bir katman her seferinde doğru kuralı uygular. Kendi yazdığın doğrulama azalır, uç durum hataları azalır, korumanın seviyesi uygulamanın her yerinde aynı olur.

Jaga’yı denemek

Jaga npm’de jagajs adıyla duruyor:

npm install jagajs

j template tag’i her değerin nereye gittiğini biliyor ve ona uyan kuralı uyguluyor:

import { j } from "jagajs";

const userUrl = "javascript:alert(1)";
const userName = '"><img src=x onerror=alert(1)>';

const html = j`
  <div title="${userName}">
    <a href="${userUrl}">Profile</a>
  </div>
`;

// <div title="&quot;&gt;&lt;img src=x onerror=alert(1)&gt;">
//   <a href="about:blank">Profile</a>
// </div>

Ham HTML için, örneğin bir zengin metin editöründen gelen çıktıda, ayrı bir sanitizer var:

import { sanitize } from "jagajs/sanitize";

const clean = sanitize(userRichText, {
  allowedTags: ["b", "i", "p", "a"],
  allowedAttrs: { a: ["href"] },
});

Bu sorunları çözmenin tek yolu Jaga değil. Ama aynı uçlarla uğraşıyorsan iyi bir başlangıç noktası olabilir, en azından probleme nasıl yaklaşılacağına dair bir referans. Kodu ve dokümantasyonun tamamı GitHub’da (yeni sekmede açılır).