Spustit prezentaci

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)?

... při programování očekáváte, že svět je zlý

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ů

Převeďte vstupní data co nejdříve na odpovídající datové typy

Příklad: problémy s řetězcovými parametry

Directory traversal (PHP)

<php?
  define ("TEMPLATE_DIR", "/home/users/phpguru/templates");
  define ("DEFAULT_TEMPLATE", "blue.tpl");

  if (isset ($_COOKIE ['TEMPLATE'])) {
    $template = $_COOKIE ['TEMPLATE'];
  } else {
    $template = DEFAULT_TEMPLATE;
  }

  include (TEMPLATE_DIR . "/" . $template);
  ...
?>

SQL injection (Java)

...
String formName = httpRequest.getFormField ("name");
...
Statement statement = connection.createStatement ();
ResultSet resultSet = statement.executeQuery (
  "SELECT email FROM member WHERE name = '"+ formName +"';");
...
Příklady pochází z Wikipedie a z článku Steve Friedla SQL Injection Attacks by Example.

Příklad: problémy s řetězcovými parametry

Directory traversal (PHP)

<php?
  define ("TEMPLATE_DIR", "/home/users/phpguru/templates");
  define ("DEFAULT_TEMPLATE", "blue");

  $templateMap = array ("blue" => "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)

...
String formName = httpRequest.getFormField ("name");
...
PreparedStatement preparedStatement = connection.prepareStatement (
  "SELECT email FROM member WHERE name = ?");
preparedStatement.setString (1, formName);
ResultSet resultSet = preparedStatement.executeQuery ();
...

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

Kontrolujte dodržování i ze své strany

Příklad: vynucení dodržování kontraktu

Kontrakt v kódu metody

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 */
}

Příklad je mírně upravený kus kódu ze softwarového projektu Davida Majdy. 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

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...

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");
  }
}

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, na kterou odkazuje článek Avoid Check for Null Statement in Java.

Příklad: vynucení dodržování kontraktu

Kontrakt v kódu metody

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

Důvěřovat (ale prověřovat) uvnitř bariéry

Doporučení pro používání assertů

Kontrola preconditions a postconditions

Situace, které nemohou nastat

Nedávejte do assertů běžný kód

Příklad: kontrola dodržování kontraktu

Kontrakt v kódu metody

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 */
}

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

Zdánlivé akceptování špatného vstupu

Příklad: zdánlivé akceptování špatného vstupu

Spojení assertů a opravy vstupu

double calculateVelocity (double latitute, 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

2. Plán A selhal, zkusit plán B

Příklad: nasazení plánu B

"Zfalšovaná" návratová hodnota

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

Uvedené členění vychází z roztomilého článku Damiena Katze Error codes or Exceptions? Why is Reliable Software so Hard?.

Proč je obsluha chyb těžká?

Zvyšuje podíl zavlečené složitosti

Způsob obsluhy chyb je součástí návrhu

Často vyžaduje vrátit věci do původního stavu

Jak zviditelňovat chyby

Obecné postupy

Práce s dynamicky alokovanou pamětí

Práce s ukazateli

Ofenzivní programování

Reakce na chyby během vývoje

Reakce na chyby v produkční verzi