SEN2008 · 14. hafta · Software Security (II): Secure software construction techniques, Security-related verification and validation

Güvenli kodlama: girdi doğrulama, SQL injection, XSS, buffer overflow, CSRF

En sık görülen beş kodlama açığını açık kod ve düzeltilmiş kod yan yana görerek öğrenirsin. Sınavda 'bu kodda ne yanlış, nasıl düzeltilir' ve 'bu girdiyle sorgu neye dönüşür' soruları buradan gelir.

input validationallowlistdenylistSQL injectionparameterized queryprepared statementcross-site scripting (XSS)output encodingContent Security Policybuffer overflowbounds checkingCSRFanti-CSRF tokenSameSite cookie

Geçen hafta tasarım kararlarını konuştuk. Bu hafta tek tek kod satırlarına iniyoruz. Sınavda neredeyse kesin gelen soru tipi: sana açık bir kod parçası verilir, “what is the vulnerability, how do you fix it?” ya da “what query is executed for this input?” diye sorulur. Beş açığın hepsi aynı kökten gelir: dışarıdan gelen veriye güvenmek ve veriyi kodla karıştırmak.

Altın kurallar:

  1. Her girdi düşmandır. Form alanı, URL, çerez, HTTP başlığı, dosya adı, API yanıtı.
  2. Doğrulamayı sunucuda yap. JavaScript kontrolü kullanıcı içindir, saldırgan onu atlar.
  3. Veriyi koddan ayır. SQL için parametre, HTML için kodlama, komut için argüman dizisi.
  4. Sınırları bil. C’de her tamponun boyutu ve \0 baytı hesapta olmalı.
  5. Durum değiştiren her istek kanıt ister. CSRF token olmadan transfer yok.

Bu sayfadaki kod parçaları açığı anlamak ve düzeltmek için. Gerçek bir sistemde izinsiz denemek suçtur.

Girdi doğrulama (input validation)

İki yaklaşım var:

YaklaşımMantıkSorun
Denylist (kara liste)bilinen kötü kalıpları reddetsaldırgan yeni kalıp, farklı kodlama, büyük-küçük harf ile aşar
Allowlist (beyaz liste)sadece beklenen biçimi kabul etbiçimi doğru tanımlamak gerekir, ama bilinmeyen reddedilir

Allowlist örneği: kullanıcı adı sadece harf, rakam, alt çizgi ve 3-16 karakter.

const USERNAME = /^[A-Za-z0-9_]{3,16}$/;
for (const s of ["deniz_07", "ab", "ali veli", "x<script>", "AAAAAAAAAAAAAAAAA"]) {
  console.log(JSON.stringify(s), USERNAME.test(s));
}
"deniz_07" true
"ab" false
"ali veli" false
"x<script>" false
"AAAAAAAAAAAAAAAAA" false

Son girdi 17 karakter, bir fazla. ^ ve $ çapaları şart: onlar olmadan regex metnin bir parçasını eşleştirir ve geri kalanı denetlenmez.

TUZAK · Doğrulama tek savunma değildir. İsim alanında O'Brien meşrudur, tırnak içerir. Bu yüzden doğrulama yanında SQL için parametre, HTML için kodlama da gerekir. Doğrulama “biçim doğru mu” sorusunu, kodlama “bu bağlamda güvenli mi” sorusunu cevaplar.

SQL injection

Sorun, sorguyu metin birleştirerek kurmak:

// AÇIK: girdi SQL metnine yapıştırılıyor
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
ResultSet rs = stmt.executeQuery(sql);

Ders kitaplarının klasik örneği: name = ' OR '1'='1. Metin şuna dönüşür:

SELECT * FROM users WHERE name = '' OR '1'='1'

'1'='1' her satırda doğru, sorgu tüm tabloyu döndürür. Üç kullanıcılı tabloda simülasyon:

rows matched (concat)   3
rows matched (param)    0

Düzeltme, parametreli sorgu:

// GÜVENLİ: sorgu yapısı sabit, girdi sadece değer
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();

Veritabanı sorguyu önce derler, ? yerine gelen değeri hiçbir zaman SQL olarak yorumlamaz. Aynı girdiyle adı tam olarak ' OR '1'='1 olan kullanıcı aranır, 0 satır döner.

Ek katmanlar: veritabanı kullanıcısına sadece gereken tablolarda gereken yetkiyi ver (least privilege), hata mesajında SQL ayrıntısı gösterme, ORM kullanıyorsan ham sorgu birleştirmekten kaçın.

TUZAK · AND/OR önceliği. ... WHERE user = 'admin' AND pass = ' + girdi + ' sorgusunda girdi x' OR 'a'='a olursa metin user = 'admin' AND pass = 'x' OR 'a'='a' olur. AND önce bağlanır: (user='admin' AND pass='x') OR 'a'='a', sonuç her satırda doğru. Çözüm yine parametre.

Cross-site scripting (XSS)

XSS’te saldırgan kendi script’ini başka bir sitenin sayfasında, o sitenin kullanıcısının tarayıcısında çalıştırır. Çerez çalma, sahte form gösterme, kullanıcı adına işlem yapma mümkün olur.

TürGirdi nerede duruyorÖrnek
Stored (kalıcı)sunucuda, veritabanındayorum alanına yazılan script, sayfayı açan herkeste çalışır
Reflected (yansıtılmış)URL’de, tek yanıttasearch?q=... değeri sayfaya kodlanmadan basılır
DOM-basedsadece tarayıcıdaJS location.hash değerini innerHTML ile yazar

Açık ve düzeltilmiş kod:

// AÇIK: kullanıcı metni HTML olarak yorumlanır
comment.innerHTML = userText;

// GÜVENLİ: metin olarak eklenir, etiket oluşturmaz
comment.textContent = userText;

Sunucu tarafında şablon kullanıyorsan çıktıyı HTML bağlamına göre kodla:

const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;")
  .replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
console.log(esc('<b>"Hi"</b>'));
console.log(esc("<script>alert(1)</script>"));
console.log(esc("Tom & Jerry's"));
&lt;b&gt;&quot;Hi&quot;&lt;/b&gt;
&lt;script&gt;alert(1)&lt;/script&gt;
Tom &amp; Jerry&#x27;s

& her zaman ilk değiştirilir; yoksa sonradan üretilen &lt; içindeki & tekrar kodlanır. Bağlam önemlidir: HTML gövdesi, HTML özniteliği, JavaScript metni ve URL için kurallar farklıdır. Modern şablon motorları (React JSX, Thymeleaf) varsayılan olarak kodlar.

İkinci katman Content Security Policy: Content-Security-Policy: script-src 'self' başlığı tarayıcıya sadece kendi origin’inden gelen script dosyalarını çalıştırmasını söyler, satır içi script engellenir. Üçüncü katman: oturum çerezine HttpOnly vermek, böylece JavaScript çerezi okuyamaz.

Buffer overflow (C)

C dizileri boyutlarını bilmez. strcpy kaynağın sonunu (\0) görene kadar kopyalar, hedefin dolup dolmadığına bakmaz.

/* AÇIK */
void greet(const char *name) {
    char buf[10];
    strcpy(buf, name);          /* uzunluk kontrolü yok */
    printf("Hello %s\n", buf);
}

name = "HelloWorld!!" (12 karakter) ise 13 bayt yazılır, 10 baytlık tamponun 3 bayt dışına taşar. Yığında tamponun ötesinde başka değişkenler ve fonksiyonun dönüş adresi durur; bunların bozulması çökme ya da kontrol akışının ele geçirilmesi demektir.

/* GÜVENLİ */
void greet(const char *name) {
    char buf[10];
    if (strlen(name) >= sizeof buf) {   /* \0 için yer kalmalı */
        fprintf(stderr, "name too long\n");
        return;
    }
    snprintf(buf, sizeof buf, "%s", name);
    printf("Hello %s\n", buf);
}
TehlikeliDaha güvenliNot
gets(buf)fgets(buf, sizeof buf, stdin)gets C11’de kaldırıldı
strcpy(d, s)snprintf(d, sizeof d, "%s", s)strncpy uzun kaynakta \0 koymaz
strcat(d, s)kalan yeri hesaplayıp snprintf
sprintf(d, ...)snprintf(d, sizeof d, ...)

Derleyici ve işletim sistemi katmanları (stack canary, ASLR, DEP/NX) saldırıyı zorlaştırır ama hatayı düzeltmez. Kalıcı çözüm sınır kontrolü ya da bellek güvenli bir dil (Java, Rust, Go).

TUZAK · off-by-one. char buf[16] için en uzun güvenli metin 15 karakterdir. if (len <= sizeof buf) yazarsan 16 karakter + \0 = 17 bayt yazılır. Doğrusu len < sizeof buf.

Cross-site request forgery (CSRF)

Kullanıcı bankaya giriş yapmış, oturum çerezi tarayıcıda. Başka bir sitede gizli bir form bankaya otomatik POST gönderir. Tarayıcı çerezi kendiliğinden ekler, sunucu isteği kullanıcıdan geldi sanır. XSS’ten farkı: saldırgan bankanın sayfasında kod çalıştırmaz, sadece kullanıcının tarayıcısına istek attırır.

<!-- AÇIK: sunucu sadece çereze bakıyor -->
<form action="/transfer" method="POST">
  <input name="to"> <input name="amount">
</form>

<!-- GÜVENLİ: oturuma bağlı, tahmin edilemez token -->
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="(sunucunun ürettiği rastgele değer)">
  <input name="to"> <input name="amount">
</form>

Sunucu her durum değiştiren istekte token’ı oturumdaki değerle karşılaştırır, uyuşmazsa reddeder. Saldırganın sayfası bu değeri okuyamaz (same-origin policy). Ek savunmalar: çereze SameSite=Lax ya da Strict vermek (çerez siteler arası POST’ta gönderilmez), kritik işlemde parola ya da MFA tekrar istemek, GET isteklerini asla durum değiştirmek için kullanmamak.

Sık yapılan hatalar

Yanlış düşünceDoğrusu
JavaScript ile doğruladım, yeterSaldırgan tarayıcıyı atlar; sunucuda doğrula
Tek tırnakları silmek SQLi’yi çözerSayısal alanda tırnak gerekmez; parametreli sorgu kullan
Prepared statement’a da metin birleştirerek sorgu verebilirimBirleştirme yaptığın an açık geri gelir; değerler sadece ? ile
innerHTML ile kullanıcı metni basmak güvenlitextContent kullan ya da bağlama göre kodla
HTTPS XSS ve CSRF’yi engellerHTTPS sadece yolu şifreler; uygulama mantığını korumaz
strncpy her zaman \0 koyarKaynak uzunsa koymaz; snprintf kullan
buf[16] 16 karakter alır15 karakter + \0
CSRF’yi XSS koruması çözerAyrı açık; token ve SameSite gerekir
Denylist yeterliAllowlist kullan; bilinmeyeni reddet

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1orta

The login code below is called with name = ' OR '1'='1. What SQL string is sent to the database?

String sql = "SELECT * FROM users WHERE name = '" + name + "'";
ResultSet rs = stmt.executeQuery(sql);
Cevabı göster
SELECT * FROM users WHERE name = '' OR '1'='1'

Girdi tırnakların arasına olduğu gibi yapıştırılır. İlk ' açık olan metni kapatır, ardından OR '1'='1 mantık ifadesi olarak eklenir, sondaki ' kodun kendi kapanış tırnağıdır:name = '' OR '1'='1'. '1'='1' her satır için doğru olduğundan WHERE tüm tabloyu döndürür. Sorun: veri ile kod aynı metne karıştı.

Soru 2kolay

What is the most reliable fix for SQL injection?

  1. Remove all single quotes from user input
  2. Use parameterized queries (prepared statements)
  3. Hide database error messages
  4. Encrypt the database
Cevabı göster
B) Use parameterized queries (prepared statements)

Parametreli sorguda SQL metni önce derlenir, girdi sonra ? yerine veri olarak bağlanır. Girdi ne içerirse içersin sorgu yapısını değiştiremez. Tırnak silmek kolay atlatılır (sayısal alanlarda tırnak bile gerekmez), hata gizlemek ve şifreleme açığı kapatmaz.

Soru 3orta

The same login query is rewritten with a prepared statement, and the table has three users: ayse, mert, deniz. With input ' OR '1'='1, how many rows are returned?

PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
Cevabı göster
0

Parametre bağlandığında veritabanı adı tam olarak ' OR '1'='1 olan kullanıcıyı arar. Böyle bir kullanıcı yok, 0 satır döner. Girdi sorgunun mantığına karışmadı, sadece aranan değer oldu.

Soru 4orta

A page inserts a user comment into HTML. Using HTML entity encoding for & < > " ', what does the comment <b>"Hi"</b> become?

Cevabı göster
&lt;b&gt;&quot;Hi&quot;&lt;/b&gt;

Her özel karakter karşılığıyla değiştirilir: < → &lt;, > → &gt;, " → &quot;. Sonuç: &lt;b&gt;&quot;Hi&quot;&lt;/b&gt;. Tarayıcı bunu etiket olarak değil düz metin olarak gösterir: ekranda <b>"Hi"</b> yazar. XSS'e karşı temel savunma bu çıktı kodlamasıdır (output encoding).

Soru 5kolay

Why is an allowlist generally preferred over a denylist for input validation?

  1. Allowlists are faster to execute
  2. A denylist must anticipate every bad input, while an allowlist only accepts known-good input
  3. Denylists cannot be written as regular expressions
  4. Allowlists encrypt the input
Cevabı göster
B) A denylist must anticipate every bad input, while an allowlist only accepts known-good input

Denylist "şu kötü kalıpları engelle" der. Saldırgan büyük-küçük harf, kodlama ya da yeni bir kalıpla listeyi aşar. Allowlist "sadece bu biçimi kabul et" der: bilinmeyen her şey reddedilir. Bu, geçen haftanın fail-safe defaults ilkesinin girdi doğrulamadaki karşılığı.

Soru 6orta

A username must fully match the pattern [A-Za-z0-9_]{3,16} (anchored at both start and end). Which input is ACCEPTED?

  1. ab
  2. ali veli
  3. deniz_07
  4. x<script>
Cevabı göster
C) deniz_07

Kural: sadece harf, rakam, alt çizgi; uzunluk 3-16.ab 2 karakter, kısa. ali veli boşluk içeriyor. x<script> < ve > içeriyor. deniz_07 8 karakter ve hepsi izinli: kabul. Not: ömer de reddedilir çünkü ö ASCII aralığında değil; gerçek sistemde bu bir tasarım kararıdır.

Soru 7zor

In the C code below, input has 12 characters. How many bytes does strcpy write, and how many bytes past the end of buf does it go?

char buf[8];
strcpy(buf, input);   /* no length check */
Cevabı göster
13 bytes written, 5 bytes past the end

strcpy sonlandırıcı \0 dahil kopyalar: 12+1=1312 + 1 = 13 bayt. Tampon 8 bayt, taşma 13−8=513 - 8 = 5 bayt. Bu baytlar komşu değişkenlerin ya da yığındaki dönüş adresinin üstüne yazılır. Düzeltme: uzunluğu kontrol et, snprintf(buf, sizeof buf, "%s", input) gibi sınırlı bir fonksiyon kullan.

Soru 8kolay

Which C function call copies a string into a fixed buffer while respecting the buffer size and always null-terminating the result?

  1. strcpy(buf, s)
  2. gets(buf)
  3. snprintf(buf, sizeof buf, "%s", s)
  4. strcat(buf, s)
Cevabı göster
C) snprintf(buf, sizeof buf, "%s", s)

snprintf en fazla sizeof buf - 1 karakter yazar ve sonuna \0 koyar. strcpy ve strcat uzunluk bilmez. gets o kadar tehlikelidir ki C11 standardından tamamen çıkarıldı. (strncpy de sınırlıdır ama kaynak uzunsa \0 koymaz, o yüzden burada doğru cevap değil.)

Soru 9kolay

An attacker's page silently submits a form to bank.example/transfer while the victim is logged in, and the victim's session cookie is sent automatically. Which defense directly stops this attack?

  1. Hashing passwords with bcrypt
  2. An unpredictable anti-CSRF token checked on every state-changing request
  3. Using HTTPS only
  4. Input length limits
Cevabı göster
B) An unpredictable anti-CSRF token checked on every state-changing request

Bu CSRF (cross-site request forgery): tarayıcı çerezi otomatik gönderdiği için sunucu isteği meşru sanar. Anti-CSRF token oturuma bağlı, tahmin edilemez bir değerdir ve formda gizli alan olarak gelir. Saldırganın sayfası bu değeri bilemez, istek reddedilir. Ek katman: çereze SameSite=Lax veya Strict vermek. HTTPS ve parola hash'i CSRF'yi engellemez.

Soru 10orta

A search page reads ?q= from the URL and writes it into the page without encoding; the payload is never stored on the server. What type of XSS is this?

  1. Stored XSS
  2. Reflected XSS
  3. DOM-based XSS only if a database is involved
  4. CSRF
Cevabı göster
B) Reflected XSS

Girdi istekle gelip aynı yanıtta geri yansıtılıyor ve kalıcı değil: yansıtılmış (reflected) XSS. Stored XSS'te girdi veritabanına kaydedilir ve sayfayı açan herkese gösterilir. DOM-based XSS'te sunucu hiç dahil olmaz; sayfadaki JavaScript girdiyi güvensiz biçimde DOM'a yazar (örn. innerHTML).

Soru 11orta

A site sends the header Content-Security-Policy: script-src 'self'. What is its effect on an injected inline <script> tag?

  1. The inline script runs normally
  2. The browser blocks the inline script because only scripts from the site's own origin files are allowed
  3. The server deletes the script from the database
  4. It encrypts the script
Cevabı göster
B) The browser blocks the inline script because only scripts from the site's own origin files are allowed

CSP tarayıcıya hangi kaynaktan script çalıştırabileceğini söyler. script-src 'self' sadece aynı origin'den yüklenen dosyalara izin verir; 'unsafe-inline' yazılmadıkça satır içi script çalışmaz. Bu, kodlama hatası kaçsa bile XSS'in etkisini azaltan ikinci katman (defense in depth). Asıl düzeltme yine çıktı kodlaması.

Soru 12zor

With char buf[16]; you copy len characters and then add a '\0'. What is the largest len that is safe, and which check is correct?

  1. 16, if (len <= sizeof buf)
  2. 15, if (len < sizeof buf)
  3. 17, if (len <= sizeof buf + 1)
  4. 8, if (len < sizeof buf / 2)
Cevabı göster
B) 15, if (len < sizeof buf)

16 baytlık tamponda \0 için bir bayt ayrılmalı: en fazla 16−1=1516 - 1 = 15 karakter. len <= 16 yazmak 16 karakter + \0 = 17 bayt demek, bir bayt taşar. Bu klasik bir off-by-one hatasıdır. Doğru kontrol len < sizeof buf.

Doğrulama: Birleştirilmiş SQL sorgularının metni, parametreli sorguyla eşleşen satır sayısı, HTML kaçış çıktıları, allowlist regex sonuçları ve tampon bayt sayıları node ile üretildi (verify/SEN2008/14.mjs).

Deftere not kartı

KURAL: girdiye güvenme, çıktıyı bağlama göre kodla, veriyi koddan ayır Doğrulama: ALLOWLIST (^[A-Za-z0-9_]{3,16}$), sunucu tarafında; istemci kontrolü sadece kolaylık SQLi: "...name = '" + x + "'" ile x = ' OR '1'='1 → name = '' OR '1'='1' (tüm satırlar) Düzeltme: PreparedStatement + ? (girdi VERİ olur, 0 satır). Tırnak silmek düzeltme DEĞİL XSS: stored (DB'de) | reflected (URL'den aynı yanıta) | DOM (JS innerHTML) Düzeltme: output encoding < → &lt; > → &gt; " → &quot; & → &amp; ' → &#x27; + CSP script-src 'self' (inline script çalışmaz) + HttpOnly çerez Buffer overflow: strcpy(buf[8], 12 karakter) → 13 bayt, 5 taşma. Sonlandırıcı \0 unutulmaz Güvenli: snprintf, uzunluk kontrolü len < sizeof buf (<= off-by-one). gets YASAK CSRF: tarayıcı çerezi otomatik yollar → anti-CSRF token + SameSite=Lax/Strict GET isteği durum DEĞİŞTİRMEZ