Makale Özeti

Güvenli kod yazmak, tıpkı ölçeklenebilir kod yazmak ya da performanslı kod yazmak kadar önemli. Belki kod yazan kişiler için sevimsiz olabilecek bir durum bu, ama aynı zamanda kaçınılmaz. Kod yazmaktaki amacımız sadece belirli istenen bir işlevin yerine getirilmesi olsaydı, kodumuzun sadece o işlevi yerine getirmesi yeterli olurdu. Oysa yazdığımız kodlar bir sistemin parçası oluyor. Bir sistemin unsurlarının tek tek işlevlerini yerlerine getirmeleri yeterli değildir. Bir arada sağlıklı bir şekilde yaşayabilmesi ve çevre şartlarına uyum gösterebilmesi gerekir sistemin.

Makale

Günümüzün vahşi dijital ortamında güvenliği göz önünde bulundurmadan yazdığımız bir kodu sahaya sürmek, Afrika jangıllarına vahşi hayvanlara karşı önlem almadan girmekten farksız.

Bu yazıda ne anlatacağımız aslında çok açık: Her yazılımcının bilmesi gereken en önemli 10 güvenlik ilkesi. (Bunu bir yerden duymuş gibi hissediyor olabilirsiniz, özetimiz aslında başlığın bir tekrarı. : )   ) Ayrıca küçük bir not: Bu yazıdan gerçek anlamda yararlanabilmek için C++, C# ve SQL konularına yakınlık sahibi olmalısınız.

1. Kullanıcı girdilerine güvenmeyin!

Güvenlik konusundaki en temel ilkelerimizden biri budur. Sağlıklı bir insan bedenini düşünün. Normal yiyeceklerle normal işlevlerini sürdürebilir. Ama sistemi bozacak denli bozuk bir gıda yerse, ölüme kadar varabilecek tehlikelerle başbaşa kalır. İyi yapılandırılmamış, beklentilerin dışında, ya da doğrudan kötü niyetli kullanıcı girdileri, kod sisteminizin çökmesine ya da kontrolu kötü niyetli bir kişiye kaptırmasına neden olabilir. Bundan sonra sıralayacağımız çok önemli kimi maddelerde tehlikenin özdeki sebebi kullanıcı girdilerinin bozuk yapıda olması ya da doğrudan kötü niyetli olarak özellikle tasarlanmasıdır.

2. "Buffer Overrun"lara karşı korunun

 Bir saldırganın uygulamanın beklediğinden daha büyük data göndermesi ve datanın iç hafıza yapısında kendisine ayrılan alandan taşması durumunda "buffer overrun" oluşur. (Buffer overrun basitçe alan taşması olarak düşünülebilir. Ancak ifade çok bilinir olduğundan bu makalede İngilizce aslıyla kullanılmıştır.)

Bu sorun temelde C/C++ dillerine ait bir sorundur. Ama bu dillerde kod yazmıyorsanız bile, altyapıda kullandığınız kimi sistemlerde olabilecek sorunlar sizin kodunuz üzerinden akıp oraya ulaşan kontrol edilmemiş data yüzünden patlayabilir.

C++ta basit bir örneğe göz atalım:

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
    char cBuffDest[32];
    memcpy(cBuffDest,cBuffSrc,cbBuffSrc);
}

Aslında cBuffSrc ve cbBuffSrcnin güvenilir olduğundan eminsek bu kodda sorun yok. Güvenilir olması ne demek bu dataların? Güvendiğimiz bir kaynak tarafından ya da en iyisi bizim tarafımızdan doğru yapıda ve doğru büyüklükte olduğunun kontrol edilmiş olması demek. Aksi taktirde, kötü niyetli bir kişi, cBuffSrcyi cBuffDestten büyük olacak şekilde ayarlayabilir. cbBuffSrcyi de cBuffDestten büyük olacak şekilde ayarlayabilir. Hafıza adres yapılarını iyi bilen ve kodunuzu tanıyacak kadar bilgiye ulaşabilmiş bir saldırgan, fonksiyonun geri dönüş adresine denk gelen kısma istediği bilgiyi yazarak kodunuzun sahip olduğu haklarla kontrolü tamamen ele geçirip kendi istediği kodların çalışmasını bile sağlayabilir.

Bu sorunun önüne geçmenin yolu, kullanıcı girdilerine güvenmemek ve gerekli kontrolleri yapmaktır:

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
    const DWORD cbBuffDest = 32;
    char cBuffDest[cbBuffDest];
#ifdef _DEBUG
    memset(cBuffDest, 0x33, cbBuffSrc);
#endif
    memcpy(cBuffDest, cBuffSrc, min(cbBuffDest, cbBuffSrc));
}


Bu kod, çağıranın bufferın uzunluğunu belirtmesine izin vermekle beraber iki seviyede önlem alır. Öncelikle debug modunda çalışıyorsa bufferın yeterlilğinin testini yapmaktadır. Böylece bir güvenlik açığını yeterince erken tespit etmiş olursunuz. İkinci olarak, normal modda çalışırken de, kaynak data hedef alandan uzunsa bile, ancak aldığı kadarını kopyalamakta, böylece komşu hafıza alanlarına taşmayı önlemektedir.

3. Cross-site Script kullanımını engelleyin

Bu tür ataklar, web yazılımlarında etkin olmaktadır. Temel mesele, kullanıcı kaynaklı datanın kontrol edilmeden siteden dışarıya gönderilen akışlarda aynen kullanılmasıdır.

Basit bir örnek:

<script language=c#>
    Response.Write("Hello, " + Request.QueryString("name"));
</script>

Bu koda mesela şöyle bir URLle erişildiğini düşünün:

http://explorationair.com/welcome.aspx?name=Michael

Hiçbir sorun yok, di mi? Gerçekten de yok.

Ama ya kullanıcı şöyle bir URLden eriştiyse sayfaya:

http://northwindtraders.com/welcome.aspx?name=<script>...</script>

Kullanıcı bile bile böyle script içeren bir adres niye girsin ki?

Ama ya kullanıcı girmediyse bu adresi? Kötü niyetli bir kişi, kendi sitesinde sizin sitenize bir link verdiyse ve linkin arka planında tıklandığında yukarıdaki gibi bir URL çağrılıyorsa? Olmayacak şey değil. Zaten oluyor da...

Buna engel olmanın ilk yolu, kullanıcı girdilerine güvenmemek ve gerekli kontrolleri regular expressionları kullanarak sağlamaktır.

Bunu yapan bir C# kod örneği:

Regex r = new Regex(@"^[\w]{1,40}$");
       
if (r.Match(strName).Success) {
    // Data sorunsuz.
} else {
    // Geçersiz data
}

Burada yapılan kontrol, girilen datanın sadece 40 karaktere kadar alfanumerik bir data olmasına izin veriyor.

Dikkat edilecek önemli bir ayrıntı: İstemediğiniz karakterleri kontrol edip onlar yoksa geçiş veren bir sistem kurmayın. Zararsız karakterleri belirleyip sadece onlara izin veren, aksi taktirde izin vermeyen bir sistem kurun.

İkinci bir yol ise, çıktı olarak kullanılan tüm girdileri HTML-encodedan geçirmektir. Bu, zararlı script taglerini daha güvenli escape karakterlerine çevirecektir. Ama yine de içeriği tam olarak kontrol edilmiş olmayacaktır.

4. sa iznini gerektiren kod kullanmayın

Girdilerin tehlikeleriyle ilgili bir başka saldırı çeşidi de SQL aşılamadır.

Şu koda bir göz atalım:

void DoQuery(string Id) {
    SqlConnection sql=new SqlConnection(@"data source=localhost;" +
            "user id=sa;password=password;");
    sql.Open();
    sqlstring= "SELECT hasshipped" +
            " FROM shipping WHERE id=" + Id + "";
    SqlCommand cmd = new SqlCommand(sqlstring,sql);
•••

3 ciddi hata var. Devam etmeden önce koda dönüp bir düşünün. Kaç tanesini bulduğunuzu bana söylemenize gerek yok : )

Birincisi: sa hesabı kullanılmış. İkincisi: sa için kullanılan parola password. Tabii evde paranızı o kadar açıkta olacağı hırsızın aklına gelmez diye masanın üzerinde bir cüzdanda tutuyorsanız, bu parola kullanımında bir sorun yok! Ama belki en önemlisi karakter birleştirme yoluyla SQLin oluşturulması. Eğer kullanıcı ID için 1001 girerse şöyle bir SQL cümlesi oluşur:

SELECT hasshipped FROM shipping WHERE id = 1001

Bunda hiçbir sorun yok, ama ya 1001 yerine şunu girerlerse: 1001 DROP table shipping --

Bakın SQL cümleniz şimdi noldu:

SELECT hasshipped FROM
shipping WHERE id = 1001 
DROP table shipping -- ;

Saldırganın kodunuzu bu hale getirebilmesi çok önemli bir güvenlik açığı. Ama zararı asıl büyüten, sa olarak bağlı olmanız. Böylece yetkide takılacağı bu işlemi, hiç takılmadan gerçekleştirebilecek!

Karakter birleştirmesiyle SQL cümleleri oluşturmayın. Stored procedureler ve onları doğru düzgün çağıran parametrik kullanımlar güvenliğinizi sağlayacaktır. Bir de: sa hesabını kullanmayın, ancak gerekli haklar tanınmış olan hesaplar oluşturup kullanın.

İşte, yukarıdaki kodun düzelmiş hali:


Regex r = new Regex(@"^\d{4,10}$");
if (!r.Match(Id).Success)
    throw new Exception("Invalid ID");

SqlConnection sqlConn= new SqlConnection(strConn);
string str="sp_HasShipped";
SqlCommand cmd = new SqlCommand(str,sqlConn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ID",Id);

5. Kripto kodlarına dikkat

Şifreleme konusunda çok orijinal fikirleriniz olduğunu düşünüp onları uyguluyor musunuz? Yoksa kendi algoritmanızı hala geliştiremediniz mi? : )

Unutmayın ki sizin gibi pek çok uygulama geliştirici sırlarını korumaya çalışıyor. Onlardan belki çok daha fazla hacker, cracker, saldırgan vb de bu sırları elde etmeye çalışıyor. Ya da en azından sistemlerin çalışmasında kusurlar oluşturmaya çalışıyorlar. Amerikayı yeniden keşfetmeye gerek yok. Aklınıza gelebilecek her türlü sır koruma yöntemi inanın ya daha önce düşünülmüş ve uygulanmıştır ya da uygulanmayacak kadar zayıftır.

Tabii çok çok istisnai olarak bu dediğim doğru olmayabilir. Yani gerçekten çok çok iyi bir algoritma bulmuş olabilirsiniz. O zaman ne duruyorsunuz, bırakın uygulama geliştirmeyi falan, geliştirdiğiniz algoritmayla zengin olmaya bakın. Hemen şirketinizi kurun, uluslararası kazanımlar için, ya da çok büyük bir uluslararası yazılım şirketine algoritmanızı satın.

Eğer bu kadar iddialı değilseniz... Bir daha düşünün. Fikrinizi çöpe atın. Size sağlanan test edilmiş ve güvenli algoritmaları kullanın. Bu, çok daha güvenli. Onları iyi öğrenip doğru kullanmakla geçirmeniz gereken zamanı kendi algoritmanızı geliştirip kullanmak hayaliyle boşa harcamayın.

6. Hedef küçültün!

Eğer bir özelliğe müşterilerin yüzde 90ının ihtiyacı yoksa, o özelliği normal kurulumda yüklemenize gerek yoktur. Böylece açık verme ihtimaliniz olan alanı azaltmış olursunuz. Cephede hedef küçültmekten çok da farklı olmayan ve benzer şekilde hayat kurtarabilecek bir önlemdir bu. Eğer bir özellik normal kurulumla yüklenecekse, yani uygulamanızın kurulu olduğu nerdeyse her yerde çalışıyor olacaksa, o zaman en küçük ayrıcalık setini kullanmayı benimseyin. Başka bir deyişle, eğer gerçekten gerekmiyorsa, admin haklarıyla kod çalıştırmayın. Ve inanın: Aslında sizin düşündüğünüzden çok çok daha az durumda gerçekten admin haklarına ihtiyaç vardır.

7. Mümkün olan en düşük ayrıcalıkları kullanın

.Netin güvenlik konusunda getirdiği sağlam temellerin sebebinde, size ya da kodunuza olan güvensizlik yatmıyor. Üzerinize alınmayın ama elbette programcılar ve yazdıkları kodlar da belki aldırmazlık gibi sebepler ya da ufak tefek kötü niyetlerle ya da tamamen iyi niyetli olmakla birlikte hatayla kötü şeylere sebep verebilirler ve güvenlik sınırlamalarıyla bu hasarın kontrol altında tutulması önemlidir... Ama asıl sebep, kodunuzda olabilecek açıkların kötüye kullanımıyla gerçekten kötü niyetli kişilerin, sizin kodunuza sizin düşünmediğiniz şeyler yaptırmasıdır. Bu durumda kodunuz, kanserli bir hücre gibi bulunduğu sisteme yönlendirmeyle ya da başıboş olarak zarar vermeye başlayabilir.

Bu anlamda önemli ilkelerden birisi, kodunuzun sadece gerekli hakları kullanıyor olmasıdır. Politikayı öyle ayarlamalı ki, admin haklarına ihtiyacı olmayan bir kod bu haklara sahip olmamalı. Ya da mesela internet çıkışına ihtiyacı duymayan bir koda bu hakkı baştan yasaklamalı. Böylelikle kodunuzu niyet ettiğiniz kullanımlardan farklı kullanacak şekilde kırabilenler, kısıtlı hakların ördüğü duvarların içinde zararlı olabilirler ancak. Hasar kontrolü sağlanmış olur.

İlkeyi şöyle ifade edebiliriz:

Bir hak verilmesi gerekiyorsa, bunu sadece gerekli olan en az miktarda koda, sadece gerekli olan en kısa zaman dilimi için verin.

Hatalar iki türlüdür, yapmamanız gereken bir şeyi yapabilir ya da yapmanız gereken bir şeyi yapmayabilirsiniz. Buraya uyarlayacak olursak: Vermeniz gereken bir izni vermemiş olabilirsiniz, ya da vermemeniz gereken bir izni vermişsinizdir. Diyelim ki tasarım sırasında bu hatanın farkına varmadınız, kod yazdınız ve testlere kadar sıra geldi. Verilmesi gerektiği halde verilmeyen bir izin, işlevsellik testlerinde kolayca ortaya çıkar. Ama vermemeniz gereken ya da verilmesi gerekmeyen bir izni verdiyseniz, bu ancak güvenlikle ilgili özel ve daha zahmetli testlerle ortaya çıkabilir. Ya da hiç ortaya çıkmaz ve kodunuz kullanımdayken kötü niyetli bir kişi tarafından keşfedilme riski siz farkında olmadan başınızın üstünde Demoklesin kılıcı gibi sallanır.

Kodunuzun içinde yüksek ayrıcalıklar kullanması gereken kısımlar varsa, bunları ayırmayı ve bir servis olarak çalıştırmayı düşünün. Böylelikle yüksek ayrıcalık isteyen kısımları daha korunaklı bir hale getirebilirsiniz. Ama bilmelisiniz ki, bu yapınızı bir parça karmaşıklaştıracaktır. Herşeyin bir bedeli var. Daha güvenli olmakla birlikte bu tarzı kullanırsanız servis arayüzlerini iyi tasarlayarak sınırlar arası gitgelleri en aza indirmeniz gerekir, aksi taktirde performans sorunlarıyla karşılaşabilirsiniz.

.Net frameworkünü kullanarak kodlama yaparken, ayrı güvenlik seviyeleri isteyen parçalarınızı ayrı assemblyler olarak oluşturursanız, güvenlik ayarlarını assembly seviyesinde daha rahat yapabilirsiniz. .Net frameworkte güvenlik altyapısı çok güçlüdür. Ama bu makalemizin konusu, daha çok genel güvenlik ilkeleri olduğundan teknik detaylarını bir başka makaleye bırakıyoruz.

Yine de bir ipucu: Eğer daha önce Code Access Securityi hiç kullanmadıysanız ve .Netle kod geliştiriyorsanız, işe güce kısa bir süre ara verip bu konuya çalışın.

8. Nasıl düştüğünüze dikkat edin!

Çoğu sporda ilk öğretilen şeylerden biri nasıl düşeceğinizdir. O güzel hareketleri, gol atmayı, basket atmayı, sayı yapmayı sağlayan şeyleri hızla öğrenmek yerine vaktinizin bir kısmında nasıl düşeceğinizi öğrenmeniz, çalışmanız gerekir. Bu size oynadığınız oyunda sayı kazandırmaz, ama eve kırık bir kolla dönmenizi engelleyebilir.

Kod yazarken de ne yazık ki, işlerin ters gitme ihtimalini düşünmek zorundayız. Belki yazdığınız hata yakalama kodları sevimli gözükmüyor size, normal iş yapma süreçleri için gerekli satırları yazmaya göre bir çeşit angarya gibi görüyor olabilirsiniz. Ama işler ters gittiği zaman o satırlar kodunuzun ne kadar iyi düşebildiğini belirleyen satırlar olacaktır. Hata yakalama kodlarını da normal kodlar kadar iyi düşünerek, detaya önem vererek yazmalı ve normal kodlar kadar iyi test etmeliyiz.

Bu konuda temel olarak dikkat etmemiz gereken noktaları sayacak olursak: Öncelikle az önce belirttiğimiz gibi, hata yakalama kodlarını da tüm kodlar kadar değerli görüp özenli yazmalıyız. İkinci olarak tüm hata yazma kodlarımızı da debug modunda satır satır incelemeyi ihmal etmemeliyiz. Çeşitli hata durumlarını oluşturup bunların akışını izlemek önemlidir. Zamanlamaya bağlı olarak oluşabilecek ve debug incelemesinde kolaylıkla oluşturamayacağımız hata durumları olabilir ama yine de kodu satır satır inceliyor olmak tekrar ve daha iyi düşünmemizi sağlayarak önemli düzeltmeler yapmamıza imkan tanıyabilir. Son olarak da test süreçlerimizde kodumuzun hata yapması için yeterince zorlanmasını sağlamalıyız. Bunu sizin yerinize bir saldırganın yaptığınızı düşünsenize. Kodunuza alışılmadık hatalar yaptırmaya çalışan bir kişi, elde etmesini istemediğiniz bilgilere ulaşmak gibi pek çok şey yapabilir.

Son bir uyarı: Eğer kodunuz hatalı bir durum oluşturursa, üzerinde çalıştığı sistemi en güvenli şartlarda bırakacak şekilde sonlanmasını sağlayın.

Bir örnek ele alalım:

bool accessGranted = true; // iyimser!
try {
    // bakalım c:\test.txt dosyasına erişimimiz var mı?
    new FileStream(@"c:\test.txt",
                    FileMode.Open,
                    FileAccess.Read).Close();
}
catch (SecurityException x) {
    // access denied
    accessGranted = false;
}
catch (...) {
    // başka bir şey oldu...
}

Dilerseniz önce kodu bir kez daha inceleyin, hatalı yaklaşımın ne olduğunu bulmaya çalışın.

//incelemeniz için ara : )

Diyelim ki, Common Language Runtime açısından dosyaya erişimimiz var, bu durumda SecurityException oluşmayacaktır. Ama diyelim ki dosyanın kendi DACLsi (Discretionary Access Control List) erişim izni vermiyor. Bu durumda SecurityException değil başka tür bir exception oluşacaktır. Oysa biz olumlu yaklaşımla, öncelikle accessGrantedı true kabul ettik ve ancak securityException durumunda bu değeri falsea çevirdik.

Aynı kodu daha iyi nasıl yazabilirdik:

bool accessGranted = false; // kötümser!
try {
    // see if we have access to c:\test.txt
    new FileStream(@"c:\test.txt",
                    FileMode.Open,
                    FileAccess.Read).Close();
     // hala buradaysak sorun yok demektir!
     accessGranted = true;
}
catch (...) {}

Gördüğünüz gibi bu durumda ancak herşey yolunda giderse accessGrantedı doğru kabul ediyoruz. (VB.netde güvenlik uygulaması ya da C#ta güvenlik uygulaması sınavlarına (70-330 ve 340) girmeyi düşünüyorsanız bu konuya ayrıca dikkat gösterin. Her iki sınavın betasında kötümser yaklaşımı kullanma üzerine birkaç soru vardı.)

9. "Impersonation" hassastır

Sunucu uygulamaları yazarken sık sık Windowsun kullanışlı bir özelliği olan impersonationa başvurabilirsiniz. Impersonation, bir processteki threadlerin ayrı güvenlik çerçevesinde çalışmasına izin verir, ve bunu genellikle uygulamanızın kullanıcısının kimliği için kullanırsınız. Mesela bir dosya sistemi yönlendiricisini düşünün. Uzaktaki kullanıcının bilgilerini güvenli olan yerel sisteme aktarır. Bu bilgiler, yerel sistemde ulaşılmak istenen dosyanın izin bilgileriyle karşılaştırılır ve tutarlı ise erişim izni verilir, aksi taktirde uzaktaki kullanıcıya hata döndürülür. Burada impersonationın kullanımı dosya sistemi yönlendiricisinin işini çok kolaylaştırmıştır.

Web uygulamalarında impersonation kullanmak için web.config dosyasında:

<identity impersonate=true>

etiketini kullanabilirsiniz. Bu durumda iki farklı güvenlik çerçevesi olan bir ortamda çalışıyorsunuz demektir. Bir process belirteciniz bir de thread belirteciniz olur. Erişim kontrolleri için thread belirteci kullanılır. Çünkü bu programınızın o anki çalışması hangi uzak kullanıcıyla ilgiliyse o uzak kullanıcının belirtecidir.

Bir ISAPI uygulamasında bu durum ilginç sonuçlar doğurabilir. Diyelim ki bir saldırgan kodunuzda açık buldu. Sizce thread belirtecindeki haklarla yetinecek midir? Tipik olarak -çoğu talebin kimliği ispatlanmamış olduğu için- bu belirteç IUSR_MACHINE olacaktır. Ama process belirteciniz SYSTEMdir! Diyelim ki buffer overflow kullanarak kontrolü ele geçiren saldırgan RevertToSelfi çağırdı... Bu durumda impersonation belirteci aradan çıkar ve process belirtecine dönülür. Yapabileceği bir başka iş de CreateProcessi çağırmaktır. Yeni process için kullanılacak belirteç threadden değil processten alınacak, yani yeni process SYSTEM olarok çalışacaktır.

Bu durumla ilgili olarak IIS 6.0 öncesinde, web uygulamanızı orta ya da yüksek izolasyonda çalışmak üzere ayarlayabilirsiniz. IIS 6,.0da ise, hiç bir kullanıcı kodu normal ayarlarda SYSTEM olarak çalışmaz. Bu, uygulama geliştiricilerin yapabilecekleri hataların etkisini kısıtlamak adına önemli bir değişikliktir.

COM ve DCOM uygulamalarında da impersonation bazı senaryolarda güvenlik açıklarına sebep olabilmektedir. Evet, impersonation kullanışlı bir özellik, ama aynı zamanda güvenlik açısından hassas bir özellik. Dikkatli kullanmanız tavsiye edilir.

10. Admin olmayanların kullanabileceği uygulamalar yazın

Uygulamanız admin hakları gerektirmemesine rağmen, dikkatsizliğiniz yüzünden, ya da konuya yeterince önem vermediğiniz için, admin olmayan bir hesapla uygulamanız çalıştırıldığında bir takım sorunlar çıkıyor olabilir. Bu durumu önceden kontrol etmeniz ve admin haklarına sahip olmadan sadece gerekli özel haklara sahip kullanıcıların uygulamanızı sorunsuz olarak çalıştırabilmenizi sağlamanız gerekir. Aksi taktirde, kullanıcılar gerekli güvenlik bilincine sahip olsalar bile, uygulamanızı çalıştırmak için admin hesapları kullanmak zorunda kalacaklardır. Böylece, uygulamanızda ortaya çıkabilecek bir açığın zarar potansiyeli çok daha yüksek olacaktır.

Bu konunun önemini hissedebilmek için, kendi uygulama geliştirdiğiniz bilgisayarınızda gerekmedikçe admin hesabı yerine normal bir hesabı kullanmayı alışkanlık haline getirin. Uygulamalarınızın kullanıcılarıyla empati oluşturabilmenizi sağlayacaktır. : )

Mustafa Acungil