# Doporučené postupy v programování ## Defenzivní programování ### Kontrola vstupu · Použití assertů · Ošetření chyb #### Lubomír Bulej ##### KDSS MFF UK
# V čem spočívá defenzivní programování? ## ... v čem spočívá defenzivní řízení (auta)? - nemůžete si být jisti tím, co udělají ostatní - minimalizace škod bez ohledu na to, kdo udělal chybu ## ... při programování očekáváte, že svět je zlý - budete dostávat špatná vstupní data - uživatelé budou porušovat kontrakt - cílem je nepoškodit vlastní data a stav - pokud to jde, reagovat benevolentně Note: Kdysi se při programování uplatňoval princip "garbage in, garbage out", což značně zjednodušuje práci programátorovi, ale v dnešní době už takový přístup nestačí. U dobrých programů se uplatňuje princip "garbage in, nothing out", případně "no garbage in".
# Vybírejte si data, se kterými budete pracovat ## Kontrolujte veškerá data z externích zdrojů - vstupy od uživatele, data ze souborů, data za sítě, ... - obor hodnot datového typu vs. přípustný obor hodnot - zvýšená opatrnost doporučena při práci s řetězci ## Převeďte vstupní data co nejdříve na odpovídající datové typy * řětězcové parametry – volná vazba na vstupu služby - logické hodnoty "yes", "no", "true", "false", ... - výčtové hodnoty "red", "green", "blue", "#C0C0C0", ... * převod na odpovídající typy vyžaduje kontrolu * práce s odpovídajícími typy je jednodušší a bezpečnější
# Příklad: problémy s řetězcovými parametry ## Directory traversal (PHP) – problém ```php bad-code stretch ``` ## SQL injection (Java) – problém ```java bad-code stretch ... String formName = httpRequest.getFormField ("name"); ... Statement statement = connection.createStatement (); ResultSet resultSet = statement.executeQuery ( "SELECT email FROM member WHERE name = '"+ formName +"';"); ... ``` Note: Příklady pochází z [Wikipedie](http://en.wikipedia.org/wiki/Directory_traversal) a z článku Steve Friedla [SQL Injection Attacks by Example](http://www.unixwiz.net/techtips/sql-injection.html).
# Příklad: problémy s řetězcovými parametry ## Directory traversal (PHP) – řešení ```php good-code stretch "blue.tpl", "green" => "green.tpl", ...); if (isset ($_COOKIE ['TEMPLATE'])) { $template = $_COOKIE ['TEMPLATE']; } if (empty ($template)) { $template = DEFAULT_TEMPLATE; } include (TEMPLATE_DIR . "/" . $templateMap [$template]); ... ?> ``` ## SQL injection (Java) – řešení ```java good-code stretch ... String formName = httpRequest.getFormField ("name"); ... PreparedStatement preparedStatement = connection.prepareStatement ( "SELECT email FROM member WHERE name = ?"); preparedStatement.setString (1, formName); ResultSet resultSet = preparedStatement.executeQuery (); ... ``` Note: Kontrola správnosti u řetězců může být obzvláště zapeklitá, protože obsah může být URL-encoded, může různě escapovat uvozovky, tečky, lomítka, atd. SQL se např. v Javě přímo používá poměrně málo, neboť pro cokoliv většího je vhodnější využít některý z frameworků pro perzistenci, nicméně např. v PHP je přímé používání SQL poměrně běžné. Pokud není k dispozici podpora pro přípravu SQL příkazů, je důležité vedle kontroly obsahu používat escapovací funkce, které datům znemožní "vyskočit" z uvozovek. U souborů je zase důležitá normalizace jmen a kontrola přístupu podle finálního jména. Ještě lepší je se jakékoliv konstrukci jmen přímo s použitím uživatelského vstupu vyhnout, např. přes mapovací tabulku.
# Vynucujte dodržování kontraktů ## Kontrolujte vstupní parametry metod - data přichází z jiných metod místo vnějších zdrojů - kontrola preconditions – kontext volání/vnitřní stav - cílem je nevpustit špatná data do implementace ## Kontrolujte dodržování i ze své strany - kontrola postconditions – výstupní hodnota, invarianty - typicky důležité hlavně při vývoji, u mission/safety critical aplikací i za běhu - cílem je nešířit špatná data a rychle detekovat chyby
# Příklad: vynucení dodržování kontraktu ## Kontrakt v kódu metody – explicitně ```java stretch public void log(String context, String taskID, Date timestamp, LogLevel level, String message) { if (context == null) { throw new NullPointerException("Context name is null"); } if (context.equals("")) { throw new IllegalArgumentException("Context name is empty"); } if (taskID == null) { throw new NullPointerException("Task ID is null"); } if (taskID.equals("")) { throw new IllegalArgumentException("Task ID is empty"); } if (timestamp == null) { throw new NullPointerException("Timestamp is null"); } if (level == null) { throw new NullPointerException("Log level is null"); } if (message == null) { throw new NullPointerException("Log message is null"); } File contextDir = new File(basedir, context); if (!contextDir.isDirectory()) { throw new IllegalArgumentException("Context not registered: " + context); } File taskDir = new File(contextDir, taskID); if (!taskDir.isDirectory()) { throw new IllegalArgumentException("Task not registered: " + taskID); } /* tělo metody */ } ``` Note: Příklad je mírně upravený kus kódu ze softwarového projektu Davida Majdy (http://been.objectweb.org/). Autor kódu (nikoliv David Majda) si krátce před jeho napsáním přečetl *Effective Java* a byl odhodlaný psát kód opravdu poctivě a neprůstřelně. Kód na kontrolu parametrů byl nakonec delší než samotné tělo metody (to je v příkladu vynecháno). Je na místě otázka, zda, případně kdy, se opravdu vyplatí parametry poctivě kontrolovat.
# Příklad: vynucení dodržování kontraktu ## Kontrakt v kódu metody – pomocné metody ```java stretch public void log(String context, String taskID, Date timestamp, LogLevel level, String message) { ensureValidContextName(context, "context"); ensureValidTaskID(taskID, "taskID"); Ensure.objectNotNull(timestamp, "timestamp"); Ensure.objectNotNull(level, "log level"); Ensure.objectNotNull(message, "message"); // File contextDir = new File(basedir, context); File taskDir = new File(contextDir, taskID); /* tělo metody */ } ``` ## Pomocné metody... ```java stretch public void ensureValidContextName(String context, String paramName) { Ensure.stringNotEmpty(string, "context name in " + paramName); File contextDir = new File(basedir, context); if (!contextDir.isDirectory()) { throw new IllegalArgumentException("Context not registered: " + context); } } public void Ensure.stringNotEmpty(String string, String paramName) { Ensure.objectNotNull(string, paramName); if (string.length() < 1) { throw new IllegalArgumentException(paramName + " is empty"); } } ``` 1> Note: Výše uvedený zápis je výrazně kompaktnější než dříve uvedený způsob vynucování kontraktu. Třída `Ensure` poskytuje statické metody pro kontrolu obecných parametrů, metody `ensureValidContextName` a `ensureValidTaskID` by typicky byly privátní, protože obsahují implementační detaily třídy. Samozřejmě je zde vidět určitá duplicita při vytváření objektů `File`. To by se dalo odstranit vhodnou privátní funkcí, která by takový objekt vracela a zároveň kontrolovala platnost vstupních parametrů. V některých jednoduchých případech je možné kontroly zobecnit a přesunout do společné třídy `Ensure` - v tomto případě by to znamenalo vytvořit např. metodu `Ensure.directoryExists` a té předat vytvořený objekt `File` a chybovou zprávu, která má být použita ve výjimce. Je však potřeba dát pozor na to, jak kombinovat např. textové informace a také typy vyhazovaných výjimek. Většinou však na vstupu metody vystačíme s `NullPointerException` a `IllegalArgumentException`. Podobný přístup umožňuje třída Preconditions z knihovny [Guava](https://www.baeldung.com/guava-preconditions), na kterou odkazuje článek [Avoid Check for Null Statement in Java](https://www.baeldung.com/java-avoid-null-check).
# Příklad: vynucení dodržování kontraktu ## Kontrakt v kódu metody – kontrola při použití - kontrola při použití (předání implementační metodě) ```java stretch public void log(String context, String taskId, Date timestamp, LogLevel level, String message) { File contextDir = new File(basedir, requireValidContextName(context)); File taskDir = new File(contextDir, requireValidTaskId(taskId)); /* volání implementační metody */ ... _log(Objects.requireNonNull(timestamp, "timestamp"), ...); ... } ```
# Princip bariéry ## Kontrolovat a vynucovat vně bariéry - vnější hranice – data z vnějších zdrojů, vnější rozhraní - vnitřní hranice – nedůvěryhodný (starý, neudržovaný) kód - typicky implementováno v rámci obsluhy chyb ## Důvěřovat (ale prověřovat) uvnitř bariéry - vnitřní hranice – funkce, třídy, moduly, subsystémy - přítomnost špatných dat indikuje netěsnost v "izolaci" - typicky implementováno pomocí assertů
# Doporučení pro používání assertů ## Kontrola preconditions a postconditions - pouze u důvěryhodného kódu – uvnitř bariéry - na vnějším okraji bariéry kontrakty vynucujte ## Dokumentace situací, které nemohou nastat * pokud nastanou, indikuje to chybu v kódu * obsluha chyb pokrývá očekávané situace – při zpracování dat * v některých případech je vhodné zabránit odstranění assertu - `throw new AssertionError (message)` vs. `assert condition : message` ## Nedávejte do assertů běžný kód - kód uvnitř assertů bývá většinou odstraněn
# Příklad: kontrola dodržování kontraktu ## Kontrakt v kódu metody – pomocí assertů ```java stretch public void log(String context, String taskID, Date timestamp, LogLevel level, String message) { assertValidContextName (context, "context"); assertValidTaskID (taskID, "taskID"); Assert.objectNotNull (timestamp, "timestamp"); Assert.objectNotNull (level, "log level"); Assert.objectNotNull (message, "message"); // File contextDir = new File (basedir, context); File taskDir = new File (contextDir, taskID); /* tělo metody */ } ``` Note: Podobnost s kódem v předcházejícím příkladu není vůbec náhodná. Místo "ensure" je použito "assert", které indikuje, že metoda se volá pouze uvnitř důvěryhodného kódu, a že většina kontrol z produkční verze programu zmizí, aby nezdržovala. Je realistické očekávat, že překladač, pokud zjistí, že těla volaných metod jsou prázdná, tato volání eliminuje, takže nebudou představovat zdroj zbytečné režie při volání metody.
# Ošetření chyb při kontrole vstupu ## Odmítnutí služby a indikace chyby * zpravidla nejjednodušší řešení – zero committment * indikace volajícímu chybovým kódem, výjimkou * zápis do logu, dialogové okno - detaily do logu, uživateli něco srozumitelného
## Zdánlivé akceptování špatného vstupu * když lze tolerovat soft chyby a existují rozumné návratové hodnoty - použít default nebo nejbližší platnou hodnotu - ignorovat vstup a vrátit neutrální nebo poslední hodnotu * "opravu" vstupu vždy zalogovat * u robustních systémů kombinace assertu a opravy vstupu
# Příklad: zdánlivé akceptování špatného vstupu ## Spojení assertů a opravy vstupu ```java stretch double calculateVelocity (double latitude, double longitude, double elevation) { assertValueWithinRange (latitude, LATITUDE_MIN, LATITUDE_MAX, "latitude"); assertValueWithinRange (longitude, LONGITUDE_MIN, LONGITUDE_MAX, "longitude"); assertValueWithinRange (elevation, ELEVATION_MIN, ELEVATION_MAX, "elevation"); // // Sanitize input data. Values should be within the asserted ranges, but // if a value is not within its valid range, it will be changed to the // closest legal value. // double actualLatitude = clampValueRange (latitude, LATITUDE_MIN, LATITUDE_MAX, "latitude"); double actualLongitude = clampValueRange (longitude, LONGITUDE_MIN, LONGITUDE_MAX, "longitude"); double actualElevation = clampValueRange (elevation, ELEVATION_MIN, ELEVATION_MAX, "elevation"); ... /* calculate velocity */ ... return velocity; } ```
# Ošetření chyb při zpracování dat ## 1. Všeho nechat a indikovat chybu * zpravidla nejjednodušší řešení * indikace volajícímu chybovým kódem, výjimkou * místy komplikováno nutností vrátit alokované prostředky
## 2. Plán A selhal, zkusit plán B * pokud existuje více cest k cíli - při posílání pošty selhalo spojení na primární mail server, je možné zkusit jiný – MX záznamy pro cílovou doménu * "zfalšovat" návratovou hodnotu - vrátit neutrální nebo poslední hodnotu - přeskočit vadný záznam a vrátit následující * použití "falešné" návratové hodnoty vždy zalogovat
# Příklad: nasazení plánu B ## "Zfalšovaná" návratová hodnota ```java stretch Texture loadTexture(String textureId) { assertValidTextureId(textureId, "textureId"); // File textureFile = textureIdToFile(textureId); Texture result = Texture.createFromFile(textureFile); if (result == null) { log.warn("failed to load texture %d, using error texture", textureId); result = ERROR_TEXTURE; } return result; } ```
# Ošetření chyb při zpracování dat ## 3. Chyba při rozsáhlé a složité změně stavu * nejtěžší situace – full commitment * nutno vrátit vše do původního stavu - hodilo by se přepnout čas na zpětný chod * nevracejte se zpět, prostě změny zapomeňte - defenzivní kopie všeho, co se chystáte změnit - immutable třídy resp. jejich instance - atomické změny stavu objektů – izolace postupných změn - transakce, funkcionální programování * jinými slovy – snažte se situaci převést na 1. nebo 2. případ Note: Uvedené členění vychází z roztomilého článku Damiena Katze [Error codes or Exceptions? Why is Reliable Software so Hard?](http://damienkatz.net/2006/04/error_code_vs_e.html).
# Proč je obsluha chyb těžká? ## Zvyšuje podíl zavlečené složitosti - kontroly vstupů, parametrů, návratových hodnot - obsluha chybových stavů, výjimek, ... ## Způsob obsluhy chyb je součástí návrhu - způsob obsluhy závisí na kontextu a typu programu - v rámci modulu by měl být konzistentní – nemusí se dobře snášet s abstrakcí ## Často vyžaduje vrátit věci do původního stavu - nejen vrátit prostředky, ale i částečné modifikace stavu - vyžaduje strategii a podporu ze strany návrhu
# Jak zviditelňovat chyby ## Obecné postupy - kontroly vstupu, výsledků, invariantů, situací co nemohou nastat, ... - viz. předchozí slajdy ## Práce s dynamicky alokovanou pamětí - prealokace paměti pro kritické situace - kontrolní pole ve strukturách (na začátku, na konci) - kontrolní značky v alokované paměti (na začátku, na konci) - extrém: kontrolní součty ve strukturách ## Práce s ukazateli - zapouzdření práce s ukazateli do funkcí a maker - registrace ukazatelů a kontrola jejich platnosti - pomocné proměnné pro částečné dereference
# Ofenzivní programování ## Reakce na chyby během vývoje - neošetřené chyby mají za následek pád programu - snaha umožnit chybám projevit se co nejdříve - vynucuje opravy během vývoje ## Reakce na chyby v produkční verzi - snaha nepadat při neošetřené chybě – uživatel má rozdělanou práci - detailní zápis do logu, přívětivá zmínka uživateli