Öğrencilerin MongoDB sorguları oluşturmasına olanak tanıyan bir öğrenme platformu, Node.js'in vm modülünü kullanmayı bıraktı ve artık her sorguyu yürütmeden önce bir soyut sözdizimi ağacına (AST) ayrıştırıyor. Bu değişiklik, kırılması mümkün olan bir kum havuzunu (sandbox) ortadan kaldırarak arka ucu, hatalı biçimlendirilmiş bir dizginin enjekte edebileceği rastgele kodlardan koruyor.
Orijinal kum havuzu neden başarısız oldu
İlk uygulama, canlı bir veritabanı tutamacını (handle) bir vm.runInContext çağrısı içine sarmalıyor ve kullanıcıları bir düzenli ifade (regular expression) ile kısıtlamaya çalışıyordu. Sadece find veya aggregate gibi metot isimlerine izin veriliyor; diğer tüm tanımlayıcıların filtrelenmesi amaçlanıyordu.
İki kusur bu yaklaşımı güvensiz hale getirdi:
- Regex filtrelemesi atlatılabilir. JavaScript, kodun herhangi bir özelliğe erişmek için köşeli parantez notasyonunu (
obj["constructor"]) kullanmasına izin verir. Bir saldırganFunctionyapıcı fonksiyonunu (constructor) alabilir, yeni bir fonksiyon oluşturabilir ve istediği herhangi bir kodu çalıştırabilir. Kaynak kod sayısız şekilde yeniden yazılabileceği için regex, alttaki özellik erişimini asla görmez. vmbir güvenlik sınırı değildir. Node dokümantasyonu,vm'in global nesneyi izole ettiğini ancak tüm süreci (process) izole etmediğini belirtir. Kum havuzuna canlı bir veritabanı bağlantısı enjekte ederek, bağlam (context) içindeki kod, veri yazan veya silenler de dahil olmak üzere o bağlantı üzerindeki herhangi bir metodu çağırma yeteneğini korumuş oldu. Kum havuzu, kodun ana süreci etkilemesini engellemedi.
AST tabanlı çözüm
Ekip, kod yürütme işlemini statik analiz ile değiştirdi. Sorgu dizgileri artık, kodun sözdizimsel yapısının ağaç temsili olan bir AST üreten acorn ayrıştırıcısına (parser) aktarılıyor. AST, katı bir beyaz liste (whitelist) üzerinden düğüm düğüm (node-by-node) inceleniyor:
- Sabitler (literals), diziler ve nesneler yalnızca düz değerler olarak göründüklerinde izin verilir.
- Metot çağrıları önceden tanımlanmış bir küme (
find,sort,limitvb.) ile sınırlandırılmıştır. Diğer tüm çağrılar reddedilir. - Hesaplanmış özellik erişimi (örneğin
obj[expr]) veya açıkça listelenmemiş herhangi bir düğüm türü anında hataya yol açar.
Ayrıştırıcı ham metin üzerinde değil, ağaç üzerinde çalıştığı için alternatif yazımlar veya köşeli parantez notasyonu hileleriyle kandırılamaz. Regex'i atlatabilecek bir yapıcı fonksiyon zinciri, tanınmayan bir düğüm olarak görünür ve herhangi bir kod çalışmadan önce reddedilir.
Bu, güvenlik için ne anlama geliyor
Yeni tasarım, "varsayılan olarak reddet" (deny by default) felsefesini takip ediyor:
- Yasaklananları değil, izin verilenleri tanımlayın. Metinsel izin listeleri (allow-listing) kapsamlı olamaz; ancak bir AST sonlu bir düğüm türü kümesine sahiptir, bu da kapsamlı bir kontrolü mümkün kılar.
- Kum havuzu içinde asla canlı kaynakları ifşa etmeyin. İzole edilmiş bir bağlama veritabanı tutamacı geçirmek, kum havuzundaki koda arka uca doğrudan bir hat açar. Ayrıştırıcı yaklaşımı, kullanıcı koduna asla canlı bir nesne vermez; yalnızca sorgunun niyetini çıkarır.
- Yapıyı doğrulayın, ardından güvenli bir şekilde yürütün. AST doğrulamayı geçtikten sonra platform, izin verilen çağrıları kendi güvenilir kod yolu üzerinden gerçek MongoDB sürücüsü metotlarına dönüştürür.
Bundan sonra nelere dikkat edilmeli
- Kod tabanınızdaki tüm
eval,new Functionveyavmkullanımlarını denetleyin. Bir beyaz liste bile JavaScript'in dinamik yapısı tarafından boşa çıkarılabilir. - Mümkün olan her yerde kullanıcı tarafından oluşturulan kodlar için AST ayrıştırmayı benimseyin.
acorn,esprimaveyababel-parsergibi kütüphaneler bu dönüşümü kolaylaştırır. - Canlı nesnelerin ifşa edilmesini sınırlayın. Bir veritabanı bağlantısı, dosya tutamacı veya ağ soketi erişilebilir olmak zorundaysa, bunu yalnızca izin vermeyi amaçladığınız minimum metotları sunan bir proxy içine alın.
- Uç durumların (edge cases) testini otomatize edin. Ayrıştırıcınızın bunları reddettiğini doğrulamak için köşeli parantez notasyonu, hesaplanmış anahtarlar veya prototip manipülasyonu kullanan sorgular oluşturun.
Özet
Bir kum havuzu olarak vm.runInContext'e güvenmek sahte bir güvenlik hissi verir; regex filtreleri JavaScript'in esnek sözdizimini kapsayamaz ve kum havuzu süreç kaynaklarını izole etmez. Kullanıcı girdisini bir AST'ye ayrıştırmak ve yalnızca anladığınız düğümlere izin vermek (whitelisting), kötü amaçlı kodu çalışmadan önce durduran somut ve sürdürülebilir bir engel sağlar. Eğer platformunuz kullanıcıların kod yazmasına izin veriyorsa, eval tarzı yürütmeyi bugün statik analiz ile değiştirin.
