Ostatnio dodawałem prostą funkcję do pewnej aplikacji webowej: umożliwienie ściągnięcia pliku z dysku. Były to pliki Excela i znajdowały się w katalogu ~/App_Data/reports.
Siłą rzeczy takie pliki nie mają ID. Mają nazwę. I po nazwie właśnie je się ściągało.
Kod otwierający plik do ściągnięcia można by napisać tak:
public Stream OpenFile(string fileName) { string reports_dir = HttpContext.Current.Server.MapPath("~/App_Data/reports"); string full_path = Path.Combine(reports_dir, fileName); return File.OpenRead(full_path); }
Użytkownik klika na jakiś link http://app.com/download?fileName=report1.xlsx i dostaje ładne okienko do ściągnięcia.
Piknie? Piknie!
Ale! Wejdźcie teraz na link
http://app.com/download?fileName=../../web.config . I co? Już nie tak piknie, prawda?
Takie zagrożenie siedzi u mnie w głowie od roku 2008, kiedy to na ówczesnym silniku mojego bloga (BlogEngine.NET) wykryto security issue zafixowany w tym commicie: http://blogengine.codeplex.com/SourceControl/changeset/41ac0cea1129. Nieźle, co? Setki blogów stojących na BE.NET miały hasła administratora plaintextem wpisane w web.config (bo nie było innej możliwości). A jakiś cwaniak zauważył, że gdzieś w silniku, w środku, siedzi sobie JavaScriptHandler.cs serwujący pliki JS na podstawie nazwy. Tyle że nie sprawdza czy zwraca faktycznie JSy i zwracał cokolwiek się chciało.
Dość prosto można ominąć taką lukę albo sprawdzając rozszerzenia żądanych plików (jak to zrobiono w BE.NET) albo bardziej… “pro”, moim zdaniem. O tak:
public Stream OpenFile(string fileName) { string reports_dir = HttpContext.Current.Server.MapPath("~/App_Data/reports"); var reports_dir_info = new DirectoryInfo(reports_dir); var fileInfo = reports_dir_info.EnumerateFiles(fileName).Single(); return fileInfo.OpenRead(); }
DirectoryInfo nie pozwoli wyjść “w górę” i udostępnia tylko pliki które faktycznie w danym katalogu się znajdują.