Java kullanmayı keyifli kılan şeylerden bir tanesi güvenli bir dil olmasıdır. C ve C++ dillerinde karşılaşabileceğiniz gösterge problemleri, bellek taşmaları ve yine bellekte yaşanabilecek bozulmalara karşı dirençlidir. Güvenli bir dil kullanırken, programın başka yerlerinde ne olursa olsun yazdığınız bir sınıftan yaratılan nesnelerin durumlarının sizin koyduğunuz kurallar çerçevesinde şekillenmesini sağlayabilirsiniz. Bunu yapmak bütün belleği kocaman bir diziymiş gibi ele alan dillerde mümkün değildir.
Güvenli dillerde bile, diğer sınıfların etkilerinden korunabilmek için biraz çaba sarfetmeniz gerekir. Bu sebeple, sınıfınızın bütün istemcilerinin kötü niyetli bir biçimde nesneyi olmaması gereken durumlara sokmaya çalışacakları varsayımı ile defansif kodlama yapmak gerekmektedir. Sistemlerin güvenliğini kırmaya çalışan kişilerin sürekli arttığını düşünürsek bu daha da önemli hale gelmektedir. Ancak bundan daha yaygın olanı iyi niyetli programcıların yanlışlıkla yapabileceği hatalar sonucu programda yol açabileceği sorunlardır. Her ne olursa olsun, sınıf yazarken bu olasılıkları göz önünde bulundurmalı, hatalara ve saldırılara karşı dirençli kodlar yazmalıyız.
Bir nesnenin kendisinden yardım almadan o nesnenin durumunda keyfi değişiklikler yapmak mümkün olmasa da, farkında olmadan bu imkanı istemcilere vermek çok kolaydır. Örneğin, aşağıdaki değiştirilemez (immutable) gibi görünen ve bir zaman aralığını temsil eden sınıfı ele alalım:
// Bozuk "immutable" sınıf
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
... // Sınıfın geri kalanı önemsizdir
}
İlk bakışta bu sınıf değiştirilemez (immutable) ve nesnelerinde start
değerinin end
değerinden büyük olamayacağını garanti ediyormuş gibi görünüyor. Ancak Date
nesneleri değiştirilebilir oldukları için sınıfta kalıyor:
// Period nesnesinin içeriğini değiştiren saldırgan kod
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p nesnesinin içeriği değişti!!
Java 8’den itibaren bu problemi çözmenin en bariz yolu Date
yerine Instant
, LocalDateTime
veya ZonedDateTime
kullanmaktır çünkü bu sınıflar değiştirilemez (immutable) özelliğe sahiptir. Date
artık yeni kodlar yazarken kullanılmaması gereken bir sınıftır. Ancak bu problem Date
sınıfına özgü değildir. Sınıflarınızı yazarken başka değiştirilebilir türler kullanmanızı gerektiren durumlar olacaktır. Böyle durumlarda bu yazıda anlatılan yöntemleri kullanmanız gerekmektedir.
Period
nesnelerinin durumunu keyfi değişikliklerden korumak için, yapıcı metoda geçilen her değiştirilebilir parametrenin koruyucu bir kopyasını yaratmak gerekir. Sınıfın geri kalanında da istemcinin gönderdiği orijinal parametreleri değil, yaratılan kopyaları kullanmalıyız.
// Düzeltilmiş yapıcı metod - koruyucu kopyaları kullanıyoruz.
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(this.start + " after " + this.end);
}
}
Düzeltilmiş yapıcı metodu kullandığımızda, bir önceki istemci kodu Period
nesnesi üzerinde bir değişikliğe sebep olmayacaktır. Dikkat ederseniz parametrelerin geçerliliğini kopyaları yarattıktan sonra test ettik (Madde 49). Bu garip görünse de aslında gereklidir. Aksi durumda parametreler test edildikten hemen sonra ve kopya oluşturmadan hemen önceki zaman diliminde, başka bir thread bu parametreleri keyfi bir biçimde değiştirebilirdi. Bilgisayar güvenliği alanında bu saldırı denetim-zamanı/kullanım-zamanı (time-of-check/time-of-use) saldırısı olarak bilinir.
Koruyucu kopyalar yaratırken Date
sınıfının clone
metotunu kullanmadığımızı da gözden kaçırmayın. Date
fınal bir sınıf olmadığı için, clone
metodu java.util.Date
türünden bir nesne döndürmeyebilir. Bunun yerine bir saldırganın tasarladığı kötü niyetli başka bir alt türün nesnesini döndürebilir. Saldırgan bunu kullanarak nesne durumlarına erişim sağlayabilir ve keyfi değişiklikler yapabilir. Bu sebeple, eğer koruyucu kopyasını yarattığınız nesne kötü niyetli kişiler tarafından kalıtılmaya açıksa clone
metodunu kullanmayın!
Her ne kadar düzelttiğimiz yapıcı metotla bir önceki istemci kodunun nesne durumunda değişiklik yapabilmesini engellesek de, Period
sınıfı hala tam güvenli değildir. Çünkü erişim metotları değiştirilebilir alanları istemcilere açmaktadır:
// Period nesnesinin durumunu değiştiren ikinci saldırı
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p nesnesinin içeriği değişti!!
Bu saldırıya önlem almak için de değiştirilebilir alanlara erişim sağlayan metotları koruyucu kopyalar döndürecek şekilde değiştirebiliriz:
// Düzeltilmiş erişim metotları - koruyucu kopya döndürüyoruz
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
Düzelttiğimiz yapıcı metot ve erişim metotları sayesinde artık Period
sınıfının nesneleri değiştirilemez hale gelmiştir. Böylece istemci kötü niyetli de olsa, hata da yapsa bütün Period
nesnelerinde start
değerinin end
değerinden büyük olamayacağını garanti etmiş olduk. (reflection veya native metotlar kullanılarak bu yine de mümkün olabilir) Çünkü Period
nesnelerinin değiştirilebilir alanlarına istemcilerin erişimi engellenmiş, bu alanlar sınıf içerisinde kapsüllenmiştir (encapsulation).
Erişim metotlarında yaratılan kopyalar için clone
metodu kullanılabilir, çünkü burada start
ve end
alanlarının java.util.Date
türünde olduğunu, güvenlik açığı oluşturabilecek bir alt sınıftan üretilmediklerini biliyoruz. Ancak yine de Madde 13’de anlatılanlardan ötürü, nesneyi kopyalamak için yapıcı metot veya static fabrika metodu kullanmak daha mantıklı olacaktır.
Koruyucu kopyalar sadece değiştirilemez sınıflar için değildir. İstemciden alınan bir nesne referansını sakladığınız her durumda, bu nesnenin değiştirilebilir olup olmadığını kontrol edin. Eğer öyle ise, siz nesne referansını sakladıktan sonra nesnede yapılacak değişiklikleri sınıfınızın tolere edip edemeyeceğini düşünün. Eğer bu durum sizin sınıfınızda problem yaratacaksa, orijinal nesne yerine bir kopya oluşturup onu kullanmalısınız. Örneğin, istemci tarafından geçilen bir nesne referansını içeride bir Set
nesnesine ekleyecekseniz veya bir Map
için anahtar olarak kullanacaksanız, bu nesnenin sınıfınızın dışında değiştirilmesi sorunlara yol açabilir.
Bu durum nesne alanlarını istemcilere geri döndürürken de geçerlidir. Sınıfınızın değiştirilemez (immutable) olması gerekmese bile, değiştirilebilir bir nesneyi istemciye döndürürken iki kez düşünün. Unutmayın ki, diziler (arrays) de değiştirebilir yapılardır, dolayısıyla bunları istemciye döndürürken kopya oluşturmalısınız veya Madde 15’de anlatıldığı gibi dizinin değiştirilemez bir görüntüsünü (immutable view) de döndürebilirsiniz.
Burada çıkartılması gereken ders aslında şudur: mümkün olduğu sürece nesnelerinizin bileşenlerini değiştirilemez nesnelerden seçmeye çalışın. Böylece koruyucu kopyalar yaratmaktan kurtulabilirsiniz. (Madde 17) Bizim Period
sınıfı örneğinizde, Java 8 veya üzeri kullanıyorsanız Date
yerine Instant
nesneleri kullanabilirsiniz. Önceki versiyonları kullanıyorsanız da Date.getTime()
metodunun döndürdüğü long
değerini Date
referansı yerine kullanabilirsiniz.
Koruyucu kopyalar yaratmanın bir performans kaybına sebep olabileceğini de söyleyelim. Eğer bir sınıf istemcilerinin nesne üzerinde keyfi değişiklikler yapmayacağından eminse (mesela sınıf ve istemcisi aynı paketin içindeyse), koruyucu kopyalardan vazgeçilebilir. Bu gibi durumlarda sınıfın gerekli belgelemeyi yapması ve istemcilerini uyarması doğru olacaktır.
Sınıf ve istemcilerin farklı paketlerde bulunduğu bazı durumlarda da koruyucu kopyalar gerekli olmayabilir. Bazı metotlar istemciden aldıkları nesneyi kontrol altına alacak şekilde tasarlanabilirler. Tabi bunu belgeleyerek istemcilere bildirmeleri gerekir. Bu durumda istemci, metodu çağırdıktan sonra nesne üzerinde değişiklik yapmayacağını garanti eder.
Bu şekilde istemciden gelen nesneyi kendi kontrolüne alan sınıflar kötü niyetli istemcilere karşı kendilerini savunamazlar. Bu sebeple ancak istemci ve sınıf arasında bir güven söz konusu ise veya nesnede oluşacak değişiklikler sadece istemcide hasara yol açacaksa kullanılmalıdırlar.
Özetle, eğer bir sınıf istemciden gelen veya döndürülen değiştirilebilir bileşenler içeriyorsa, bu bileşenlerin koruyucu kopyaları yaratılmalıdır. Bu eğer ciddi performans kaybına yol açıyorsa veya sınıf istemcilerinin uygunsuz değişiklikler yapmayacağından eminse, koruyucu kopyalar yerine gerekli belgeleme yapılarak istemciler uyarılmalıdır.