Defensive Programmierung - Defensive programming
Defensive Programmierung ist eine Form des defensiven Designs, die darauf abzielt, die fortgesetzte Funktion einer Software unter unvorhergesehenen Umständen zu gewährleisten . Defensive Programmierpraktiken werden häufig dort eingesetzt, wo hohe Verfügbarkeit , Sicherheit oder Sicherheit erforderlich ist.
Defensive Programmierung ist ein Ansatz zur Verbesserung von Software und Quellcode in Bezug auf:
- Allgemeine Qualität – Reduzierung der Anzahl von Softwarefehlern und -problemen.
- Den Quellcode verständlich machen – Der Quellcode sollte lesbar und verständlich sein, damit er in einem Code-Audit freigegeben wird .
- Das Verhalten der Software trotz unerwarteter Eingaben oder Benutzeraktionen vorhersehbar zu machen.
Eine übermäßig defensive Programmierung kann jedoch vor Fehlern schützen, die nie auftreten werden, wodurch Laufzeit- und Wartungskosten entstehen. Es besteht auch die Gefahr, dass Code-Traps zu viele Ausnahmen verhindern , was möglicherweise zu unbemerkten, falschen Ergebnissen führt.
Sichere Programmierung
Sichere Programmierung ist die Untermenge der defensiven Programmierung, die sich mit der Computersicherheit befasst . Sicherheit ist das Anliegen, nicht unbedingt Sicherheit oder Verfügbarkeit (die Software kann auf bestimmte Weise ausfallen). Wie bei allen Arten von defensiver Programmierung ist die Vermeidung von Fehlern ein vorrangiges Ziel; die Motivation ist jedoch nicht so sehr, die Wahrscheinlichkeit von Ausfällen im Normalbetrieb zu reduzieren (als ob es um die Sicherheit ginge), sondern die Angriffsfläche zu reduzieren – der Programmierer muss davon ausgehen, dass die Software aktiv missbraucht werden könnte, um Fehler zu erkennen, und dass Fehler könnten böswillig ausgenutzt werden.
int risky_programming(char *input) {
char str[1000];
// ...
strcpy(str, input); // Copy input.
// ...
}
Die Funktion führt zu undefiniertem Verhalten, wenn die Eingabe mehr als 1000 Zeichen umfasst. Einige unerfahrene Programmierer halten dies möglicherweise nicht für ein Problem, wenn Sie davon ausgehen, dass kein Benutzer eine so lange Eingabe macht. Dieser spezielle Fehler demonstriert eine Schwachstelle, die Pufferüberlauf- Exploits ermöglicht . Hier ist eine Lösung für dieses Beispiel:
int secure_programming(char *input) {
char str[1000+1]; // One more for the null character.
// ...
// Copy input without exceeding the length of the destination.
strncpy(str, input, sizeof(str));
// If strlen(input) >= sizeof(str) then strncpy won't null terminate.
// We counter this by always setting the last character in the buffer to NUL,
// effectively cropping the string to the maximum length we can handle.
// One can also decide to explicitly abort the program if strlen(input) is
// too long.
str[sizeof(str) - 1] = '\0';
// ...
}
Offensive Programmierung
Offensive Programmierung ist eine Kategorie der defensiven Programmierung, mit dem zusätzlichen Schwerpunkt , dass bestimmte Fehler sollen nicht werden defensiv behandelt . In dieser Praxis werden nur Fehler behandelt, die außerhalb der Kontrolle des Programms liegen (wie z. B. Benutzereingaben); Die Software selbst sowie Daten aus der Verteidigungslinie des Programms sind bei dieser Methodik vertrauenswürdig .
Vertrauen in die interne Datenvalidität
- Zu defensive Programmierung
const char* trafficlight_colorname(enum traffic_light_color c) {
switch (c) {
case TRAFFICLIGHT_RED: return "red";
case TRAFFICLIGHT_YELLOW: return "yellow";
case TRAFFICLIGHT_GREEN: return "green";
}
return "black"; // To be handled as a dead traffic light.
// Warning: This last 'return' statement will be dropped by an optimizing
// compiler if all possible values of 'traffic_light_color' are listed in
// the previous 'switch' statement...
}
- Offensive Programmierung
const char* trafficlight_colorname(enum traffic_light_color c) {
switch (c) {
case TRAFFICLIGHT_RED: return "red";
case TRAFFICLIGHT_YELLOW: return "yellow";
case TRAFFICLIGHT_GREEN: return "green";
}
assert(0); // Assert that this section is unreachable.
// Warning: This 'assert' function call will be dropped by an optimizing
// compiler if all possible values of 'traffic_light_color' are listed in
// the previous 'switch' statement...
}
Vertrauenswürdige Softwarekomponenten
- Zu defensive Programmierung
if (is_legacy_compatible(user_config)) {
// Strategy: Don't trust that the new code behaves the same
old_code(user_config);
} else {
// Fallback: Don't trust that the new code handles the same cases
if (new_code(user_config) != OK) {
old_code(user_config);
}
}
- Offensive Programmierung
// Expect that the new code has no new bugs
if (new_code(user_config) != OK) {
// Loudly report and abruptly terminate program to get proper attention
report_error("Something went very wrong");
exit(-1);
}
Techniken
Hier sind einige defensive Programmiertechniken:
Intelligente Wiederverwendung von Quellcode
Wenn vorhandener Code getestet wurde und bekannt ist, dass er funktioniert, kann die Wiederverwendung des Codes die Wahrscheinlichkeit verringern, dass Fehler eingeführt werden.
Die Wiederverwendung von Code ist jedoch nicht immer eine bewährte Methode, da sie auch die Schäden eines möglichen Angriffs auf den ursprünglichen Code verstärkt. Die Wiederverwendung kann in diesem Fall zu schwerwiegenden Fehlern in den Geschäftsprozessen führen .
Legacy-Probleme
Vor der Wiederverwendung von altem Quellcode, Bibliotheken, APIs, Konfigurationen usw. muss berücksichtigt werden, ob die alte Arbeit für die Wiederverwendung gültig ist oder wahrscheinlich anfällig für Legacy- Probleme ist.
Legacy-Probleme sind Probleme, die inhärent sind, wenn von alten Designs erwartet wird, dass sie mit den heutigen Anforderungen funktionieren, insbesondere wenn die alten Designs nicht unter Berücksichtigung dieser Anforderungen entwickelt oder getestet wurden.
Bei vielen Softwareprodukten sind Probleme mit altem Legacy-Quellcode aufgetreten; zum Beispiel:
- Legacy-Code wurde möglicherweise nicht im Rahmen einer defensiven Programmierinitiative entworfen und kann daher von viel geringerer Qualität sein als neu entworfener Quellcode.
- Legacy-Code wurde möglicherweise unter Bedingungen geschrieben und getestet, die nicht mehr gelten. Die alten Qualitätssicherungsprüfungen haben möglicherweise keine Gültigkeit mehr.
- Beispiel 1 : Legacy-Code wurde möglicherweise für ASCII-Eingabe entworfen, aber jetzt ist die Eingabe UTF-8.
- Beispiel 2 : Legacy-Code wurde möglicherweise auf 32-Bit-Architekturen kompiliert und getestet, aber wenn er auf 64-Bit-Architekturen kompiliert wurde, können neue arithmetische Probleme auftreten (z. B. ungültige Vorzeichentests, ungültige Typumwandlungen usw.).
- Beispiel 3 : Legacy-Code wurde möglicherweise für Offline-Computer verwendet, wird jedoch anfällig, sobald die Netzwerkkonnektivität hinzugefügt wird.
- Legacy-Code wird nicht mit Blick auf neue Probleme geschrieben. Zum Beispiel ist Quellcode, der 1990 geschrieben wurde, wahrscheinlich anfällig für viele Code-Injection- Schwachstellen, da die meisten dieser Probleme zu dieser Zeit noch nicht allgemein verstanden wurden.
Bemerkenswerte Beispiele für das Legacy-Problem:
- BIND 9 , präsentiert von Paul Vixie und David Conrad als "BINDv9 ist ein komplettes Rewrite ", "Sicherheit war eine Schlüsselüberlegung beim Design", benennt Sicherheit, Robustheit, Skalierbarkeit und neue Protokolle als Hauptanliegen beim Umschreiben von altem Legacy-Code.
- Microsoft Windows litt unter "der" Windows-Metafile-Schwachstelle und anderen Exploits im Zusammenhang mit dem WMF-Format. Das Microsoft Security Response Center beschreibt die WMF-Funktionen als "Um 1990 wurde WMF-Unterstützung hinzugefügt... Dies war eine andere Zeit in der Sicherheitslandschaft... wurden alle vollständig vertraut" , nicht im Rahmen der Sicherheitsinitiativen bei Microsoft entwickelt.
- Oracle bekämpft Legacy-Probleme, wie z. B. alten Quellcode, der ohne Bedenken bezüglich SQL-Injection und Privilege Escalation geschrieben wurde , was zu vielen Sicherheitslücken führt, deren Behebung Zeit gedauert hat und die auch unvollständige Fixes generiert haben. Dies hat zu heftiger Kritik von Sicherheitsexperten wie David Litchfield , Alexander Kornbrust , Cesar Cerrudo geführt . Ein weiterer Kritikpunkt ist, dass Standardinstallationen (größtenteils ein Legacy aus alten Versionen) nicht mit ihren eigenen Sicherheitsempfehlungen wie der Oracle Database Security Checklist übereinstimmen , die schwer zu ändern ist, da viele Anwendungen die weniger sicheren Legacy-Einstellungen erfordern, um korrekt zu funktionieren.
Kanonisierung
Böswillige Benutzer erfinden wahrscheinlich neue Arten der Darstellung falscher Daten. Wenn ein Programm beispielsweise versucht, den Zugriff auf die Datei "/etc/ passwd " abzulehnen , könnte ein Cracker eine andere Variante dieses Dateinamens wie "/etc/./passwd" übergeben. Kanonisierungsbibliotheken können verwendet werden, um Fehler aufgrund nicht- kanonischer Eingaben zu vermeiden .
Geringe Toleranz gegenüber "potenziellen" Fehlern
Gehen Sie davon aus, dass Codekonstrukte, die anscheinend problemanfällig sind (ähnlich wie bekannte Schwachstellen usw.), Fehler und potenzielle Sicherheitslücken sind. Die Faustregel lautet: „Ich bin mir nicht bewusst , alle Arten von Sicherheitslücken ich gegen diejenigen schützen , muss ich. Sie kenne , und dann muss ich proaktiv sein!“.
Andere Techniken
- Eines der häufigsten Probleme ist die ungeprüfte Verwendung von Strukturen und Funktionen mit konstanter Größe für Daten dynamischer Größe (das Pufferüberlaufproblem ). Dies ist besonders häufig bei Zeichenfolgendaten in C der Fall . C-Bibliotheksfunktionen wie
getssollten niemals verwendet werden, da die maximale Größe des Eingabepuffers nicht als Argument übergeben wird. C-Bibliotheksfunktionen wiescanfkönnen sicher verwendet werden, erfordern jedoch, dass der Programmierer bei der Auswahl sicherer Formatzeichenfolgen sorgfältig vorgeht, indem er sie vor der Verwendung bereinigt. - Verschlüsseln/authentifizieren Sie alle wichtigen Daten, die über Netzwerke übertragen werden. Versuchen Sie nicht, Ihr eigenes Verschlüsselungsschema zu implementieren, sondern verwenden Sie stattdessen ein bewährtes.
- Alle Daten sind wichtig, bis das Gegenteil bewiesen ist.
- Alle Daten sind verfälscht, bis das Gegenteil bewiesen ist.
- Der gesamte Code ist bis zum Beweis des Gegenteils unsicher.
- Sie können die Sicherheit eines Codes in userland nicht beweisen , oder kanonischer: "Vertrauen Sie niemals dem Client" .
- Wenn Daten auf Richtigkeit überprüft werden sollen, überprüfen Sie, ob sie richtig sind, nicht, dass sie falsch sind.
-
Design nach Vertrag
- Design by Contract verwendet Vorbedingungen , Nachbedingungen und Invarianten, um sicherzustellen, dass die bereitgestellten Daten (und der Zustand des Programms als Ganzes) bereinigt werden. Dies ermöglicht es dem Code, seine Annahmen zu dokumentieren und sie sicher zu machen. Dies kann bedeuten, dass Argumente für eine Funktion oder Methode auf Gültigkeit geprüft werden, bevor der Hauptteil der Funktion ausgeführt wird. Nach dem Rumpf einer Funktion ist es auch ratsam, den Status oder andere gehaltene Daten und den Rückgabewert vor dem Verlassen (Break/Return/Throw/Fehlercode) zu überprüfen.
-
Assertionen (auch Assertive Programmierung genannt )
- Innerhalb von Funktionen möchten Sie möglicherweise überprüfen, ob Sie nicht auf etwas Ungültiges (dh null) verweisen und dass die Array-Längen gültig sind, bevor Sie auf Elemente verweisen, insbesondere bei allen temporären/lokalen Instanziierungen. Eine gute Heuristik besteht darin, auch den Bibliotheken, die Sie nicht geschrieben haben, nicht zu vertrauen. Überprüfen Sie also jedes Mal, wenn Sie sie anrufen, was Sie von ihnen zurückbekommen. Es hilft oft, eine kleine Bibliothek von "Asserting"- und "Checking"-Funktionen zusammen mit einem Logger zu erstellen, damit Sie Ihren Weg verfolgen und die Notwendigkeit umfangreicher Debugging- Zyklen von vornherein reduzieren können . Mit dem Aufkommen von Protokollierungsbibliotheken und aspektorientierter Programmierung werden viele der mühsamen Aspekte der defensiven Programmierung gemildert.
- Ausnahmen von Rückgabecodes
bevorzugen
- Im Allgemeinen ist es vorzuziehen, verständliche Ausnahmemeldungen auszulösen, die einen Teil Ihres API- Vertrags erzwingen und den Client- Programmierer anleiten, anstatt Werte zurückzugeben, auf die ein Client-Programmierer wahrscheinlich nicht vorbereitet ist, und damit seine Beschwerden zu minimieren und die Robustheit und Sicherheit Ihrer Software zu erhöhen .
Siehe auch
Verweise
- ^ "Fogo-Archiv: Paul Vixie und David Conrad über BINDv9 und Internet Security von Gerald Oskoboiny <[email protected]>" . beeindruckend.net . Abgerufen am 27.10.2018 .
- ^ "Wenn Sie sich die WMF-Ausgabe ansehen, wie ist sie dorthin gekommen?" . MSRC . Archiviert vom Original am 2006-03-24 . Abgerufen am 27.10.2018 .
- ^ Litchfield, David. "Bugtraq: Oracle, wo sind die Patches???" . seclists.org . Abgerufen am 27.10.2018 .
- ^ Alexander, Kornbrust. "Bugtraq: RE: Oracle, wo sind die Patches???" . seclists.org . Abgerufen am 27.10.2018 .
- ^ Cerrudo, Cäsar. "Bugtraq: Re: [Vollständige Offenlegung] RE: Oracle, wo sind die Patches???" . seclists.org . Abgerufen am 27.10.2018 .