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.
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:
- Her girdi düşmandır. Form alanı, URL, çerez, HTTP başlığı, dosya adı, API yanıtı.
- Doğrulamayı sunucuda yap. JavaScript kontrolü kullanıcı içindir, saldırgan onu atlar.
- Veriyi koddan ayır. SQL için parametre, HTML için kodlama, komut için argüman dizisi.
- Sınırları bil. C’de her tamponun boyutu ve
\0baytı hesapta olmalı. - 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şım | Mantık | Sorun |
|---|---|---|
| Denylist (kara liste) | bilinen kötü kalıpları reddet | saldırgan yeni kalıp, farklı kodlama, büyük-küçük harf ile aşar |
| Allowlist (beyaz liste) | sadece beklenen biçimi kabul et | biç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'Brienmeş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 girdix' OR 'a'='aolursa metinuser = '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ür | Girdi nerede duruyor | Örnek |
|---|---|---|
| Stored (kalıcı) | sunucuda, veritabanında | yorum alanına yazılan script, sayfayı açan herkeste çalışır |
| Reflected (yansıtılmış) | URL’de, tek yanıtta | search?q=... değeri sayfaya kodlanmadan basılır |
| DOM-based | sadece tarayıcıda | JS 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, "&").replace(/</g, "<")
.replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
console.log(esc('<b>"Hi"</b>'));
console.log(esc("<script>alert(1)</script>"));
console.log(esc("Tom & Jerry's"));
<b>"Hi"</b>
<script>alert(1)</script>
Tom & Jerry's
& her zaman ilk değiştirilir; yoksa sonradan üretilen < 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);
}
| Tehlikeli | Daha güvenli | Not |
|---|---|---|
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ğrusulen < 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üşünce | Doğrusu |
|---|---|
| JavaScript ile doğruladım, yeter | Saldırgan tarayıcıyı atlar; sunucuda doğrula |
| Tek tırnakları silmek SQLi’yi çözer | Sayısal alanda tırnak gerekmez; parametreli sorgu kullan |
| Prepared statement’a da metin birleştirerek sorgu verebilirim | Birleştirme yaptığın an açık geri gelir; değerler sadece ? ile |
innerHTML ile kullanıcı metni basmak güvenli | textContent kullan ya da bağlama göre kodla |
| HTTPS XSS ve CSRF’yi engeller | HTTPS sadece yolu şifreler; uygulama mantığını korumaz |
strncpy her zaman \0 koyar | Kaynak uzunsa koymaz; snprintf kullan |
buf[16] 16 karakter alır | 15 karakter + \0 |
| CSRF’yi XSS koruması çözer | Ayrı açık; token ve SameSite gerekir |
| Denylist yeterli | Allowlist kullan; bilinmeyeni reddet |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
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
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ı.
What is the most reliable fix for SQL injection?
- Remove all single quotes from user input
- Use parameterized queries (prepared statements)
- Hide database error messages
- Encrypt the database
Cevabı göster
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.
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
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.
A page inserts a user comment into HTML. Using HTML entity encoding for & < > " ', what does the comment <b>"Hi"</b> become?
Cevabı göster
<b>"Hi"</b>Her özel karakter karşılığıyla değiştirilir: < → <, > → >, " → ". Sonuç: <b>"Hi"</b>. 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).
Why is an allowlist generally preferred over a denylist for input validation?
- Allowlists are faster to execute
- A denylist must anticipate every bad input, while an allowlist only accepts known-good input
- Denylists cannot be written as regular expressions
- Allowlists encrypt the input
Cevabı göster
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ığı.
A username must fully match the pattern [A-Za-z0-9_]{3,16} (anchored at both start and end). Which input is ACCEPTED?
- ab
- ali veli
- deniz_07
- x<script>
Cevabı göster
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.
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
strcpy sonlandırıcı \0 dahil kopyalar: bayt. Tampon 8 bayt, taşma 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.
Which C function call copies a string into a fixed buffer while respecting the buffer size and always null-terminating the result?
- strcpy(buf, s)
- gets(buf)
- snprintf(buf, sizeof buf, "%s", s)
- strcat(buf, s)
Cevabı göster
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.)
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?
- Hashing passwords with bcrypt
- An unpredictable anti-CSRF token checked on every state-changing request
- Using HTTPS only
- Input length limits
Cevabı göster
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.
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?
- Stored XSS
- Reflected XSS
- DOM-based XSS only if a database is involved
- CSRF
Cevabı göster
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).
A site sends the header Content-Security-Policy: script-src 'self'. What is its effect on an injected inline <script> tag?
- The inline script runs normally
- The browser blocks the inline script because only scripts from the site's own origin files are allowed
- The server deletes the script from the database
- It encrypts the script
Cevabı göster
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ı.
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?
- 16,
if (len <= sizeof buf) - 15,
if (len < sizeof buf) - 17,
if (len <= sizeof buf + 1) - 8,
if (len < sizeof buf / 2)
Cevabı göster
if (len < sizeof buf)16 baytlık tamponda \0 için bir bayt ayrılmalı: en fazla 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).