Wie man's nicht machen sollte
Felix Riesterer
- php
- sicherheit
Liebe PHP-Interessierte,
heute habe ich bei Fefe einen Beitrag gelesen, in dem er sich über eine Sicherheitslücke in Software von Palo Alto lustig macht. Dabei zeigt er PHP-Code, den er aber nicht für Laien kommentiert, weil er bei seiner Leserschaft davon ausgeht, dass die den Witz an der Sache selbst erkennen. In dem Code werden Dinge getan, wie man sie gelegentlich auch hier in Fragepostings findet. Deswegen dachte ich, dass ich davon etwas kurz bespreche.
$locale = isset($_POST['locale']) ? $_POST['locale'] : $_SESSION['locale'];
$user = $_POST['user'];
$userRole = $_POST['userRole'];
$remoteHost = $_POST['remoteHost'];
$vsys = $_POST['vsys'];
$editShared = $_POST['editShared'];
$protocol = $_POST['prot'];
$serverPort = $_SERVER['SERVER_PORT'];
$rbaXml = $_POST['rbaxml'];
$hideHeaderBg = $_POST['hideHeaderBg'];
if (strlen($user) <= 63
&& strlen ($userRole) < 256
&& strlen ($remoteHost) < 256
&& strlen ($vsys) < 128
&& strlen ($editShared) < 128
&& strlen ($protocol) < 128
&& strlen ($serverPort) < 128
&& strlen ($rbaXml) < 1024 * 1024
&& strlen ($locale) < 256
&& strlen ($hideHeaderBg) < 128
) {
panCreateRemoteAppwebSession(
$user,
$userRole,
$remoteHost,
$vsys,
$editShared,
$protocol,
$serverPort,
$rbaXml,
$locale,
$hideHeaderBg
);
} else {
error_log("[englische Fehlermeldung]");
}
Die Variable $locale
ist ein Wert, der aus zwei verschiedenen Quellen kommen kann ($_POST
oder $_SESSION
) und hat daher einen Sinn. Die restlichen neun Variablen sind reine Umbenennungen und verschleiern nur, dass die Werte aus dem $_POST
-Array kommen, also im Zweifelsfall bösartige und von einem User mit Absicht manipulierte Werte sind, die das System aushebeln sollen.
Bevor die Funktion panCreateRemoteAppwebSession
aufgerufen wird, findet noch eine Prüfung statt, ob alle Werte die erforderlichen Längenbeschränkungen einhalten. In der Bedingung der if
-Verzweigung sehen wir lauter Vergleiche, bei denen der Rückgabewert der Funktion strlen
(Anzahl Zeichen im String) mit einer Zahl verglichen wird. Diese Zahlen stehen einfach so im Code, ohne Erklärung, warum es jeweils genau diese Anzahl an Zeichen höchstens sein dürfen. Solcherlei Zahlenangaben im Code nennt man magic numbers, die zu den Unarten des Programmierens zählen.
Was an dieser Stelle wirklich hätte passieren müssen, wäre eine echte Plausibilitätsprüfung gewesen. Man kann im Ernstfall für jeden String eine eigene Prüffunktion schreiben, da die Strings offensichtlich die unterschiedlichsten Bedeutungen haben. Nehmen wir uns die einfachste Prüfung vor: Benutzername und Benutzerrolle:
function user_exists ($login) {
$available_user_logins = [];
// Hier müsste Code stehen, der alle Login-Namen in das Array schreibt.
// Also so etwas wie eine Datenbankabfrage,
// oder einen Ladevorgang aus einer Datendatei
return in_array($login, $available_user_logins);
}
function user_role_exists ($role) {
$available_roles = [];
// Hier müsste Code stehen, der alle Benutzerrollen in das Array schreibt.
return in_array($role, $available_roles);
}
In diesen beiden Plausibilitätsprüfungen kann man sehen, dass der jeweils vom Benutzer übermittelte Wert mit bereits vorhandenen Werten verglichen wird. So können bösartige Werte keinen Schaden anrichten.
// jetzt die Verzweigung mit Plausibilitätsprüfungen
if (user_exists($_POST['user'])
&& user_role_exists($_POST['userRole'])
&& ...
) {
...
} else {
...
}
Man sieht nun in der Bedingung der if
-Verzweigung, dass Benutzereingaben (also potenziell bösartige Werte) auf ihre Plausibilität hin geprüft werden. Und das individuell!
Auf eine sehr ähnliche Art und Weise kann man mit dem Wert für das Protokoll ($_POST['prot']
) verfahren, der offensichtlich aus einer Liste ausgewählt wurde. Wenn man die möglichen Werte mit dem tatsächlich empfangenen Wert vergleicht, könnte man zusätzlich die Groß-/Kleinschreibung ignorieren (z.B. HTTP im Vergleich zu http).
Wieso eigentlich darf die Port-Angabe ($_SERVER['SERVER_PORT']
) im obigen Beispiel bis zu 127 Zeichen haben? Eine Zahl mit so vielen Stellen könnte ich spontan nicht einmal korrekt benennen. Und dann gibt es da noch technische Begrenzungen, in welchem Zahlenraum sich eine Port-Angabe bewegen kann: 1 - 65535. Da hätte man besser nicht die Maximallänge des Strings geprüft, sondern den tatsächlichen Zahlenwert, den er abbildet:
function is_valid_port ($port) {
$valid_ports = [
80, // HTTP
143, // HTTPS
8080, // HTTP intranet
];
return in_array(
(int) $port, // vergleiche Ganzzahl anstatt String-Wert
$valid_ports
);
}
Nun darf man einigermaßen darauf vertrauen, dass die Werte im $_SERVER
-Array keinen Unsinn beinhalten. Wenn man selbst in seinem Code die Werte dort niemals verändert, sondern immer nur ausliest, dann sind diese einigermaßen weniger unsicher. Eine Plausibilitätsprüfung ist aber trotzdem wichtig!
Liebe Grüße
Felix Riesterer
Servus!
Vielen Dank! Wär das nix für den Blog?
Herzliche Grüße
Matthias Scharwies
PS: Sitze im Elternsprechabend über MS Teams und habe 5min Lücke. Bis jetzt
2x niemand am gebuchten Termin da,
2x mit Handy telefoniert, weil Teams nicht geht
und 12 gute Gespräche.
Aloha ;)
Danke fürs Aufschlüsseln!
Trotzdem möchte ich bei allem berechtigtem Spott zum Thema PaloAlto noch eins hier zu bedenken geben:
Kontextbehandlung ist eine Frage des Kontexts. Und der ist hier für ein abschließendes Urteil nicht hinreichend bekannt.
Zum Beispiel ist es sehr relevant, was in der Funktion
panCreateRemoteAppwebSession
passiert. Kann sein die von dir hier vermisste Kontextbehandlung passiert dort. Kann sein die hier kritisch betrachteten magic numbers erschließen sich aus der Funktionsweise bzw. den Annahmen dieser Funktion. Kann sein hier werden Bibliotheken bedient, die aus gutem Grund gekapselt sind aber im Vorfeld keine Kontextbehandlung über die reine Datenmenge (Strings mit maximal 127 Zeichen) hinaus benötigen. Kann sein der böswillige Userinput soll an dieser Stelle noch erhalten bleiben um anschließend unschädlich gemacht und geloggt zu werden. Und so weiter und so fort.
Ich empfinde das Gesamturteil an der Stelle also als verfrüht und möchte davor warnen, nur die augenscheinlichen red flags anzuschauen ohne im Detail zu wissen, ob sie im konkreten Fall denn relevant sind.
Grüße,
RIDER
Lieber Camping_RIDER,
Zum Beispiel ist es sehr relevant, was in der Funktion
panCreateRemoteAppwebSession
passiert. Kann sein die von dir hier vermisste Kontextbehandlung passiert dort.
ja, kann. Wenn Du auf den verlinkten Artikel schaust, wirst Du sehen, dass da ursprünglich keine umkopierten Variablen standen. Es ist völlig unklar, warum man diese Änderung am Code vorgenommen und an die Kunden ausgerollt hat. Für unsere Belange bei SELFHTML aber ist das ein real world-Beispiel dafür, wie man es eben nicht machen sollte. Deswegen meine Ausführungen.
Liebe Grüße
Felix Riesterer
Hallo Felix,
danke für den Post. Aber ganz ehrlich, das Einzige, was man dem kleinen Code-Schnipsel vorwerfen kann, ist die fehlende Information/inline Kommentierung, was welche Variable beinhaltet bzw. bezweckt. Und FeFe schreibt auch viel Unsinn, wenn der Tag lang ist.
Ohne einen genauen Überblick zu haben, an welcher Stelle und Pipeline in der Architektur dieses Stück Code sitzt, ist es irgendwie sinnfrei über Sicherheit zu diskutieren. Ich habe mir mal den Originalpost durchgelesen, die gewichtigen Fehler werden da an ganz anderer Stelle gemacht.
Das man Request-Headern nicht blind vertrauen darf, sollte bekannt sein. Aber der hier aufgeführte Code hat wohl nicht die Aufgabe, dafür Sorge zu tragen. Und PHP ist eine ziemlich blöde Idee für ein Backend (meine persönliche Meinung).
Die Kernbotschaft ist doch eigentlich stets die Gleiche: Beachte, ob man den Inputdaten vertrauen kann, wenn nicht, dannn müssen die Daten zuvor ausreichend validiert werden. Was "ausreichende" Validierung bedeutet, ist davon abhängig, an welcher Stelle in der Architektur der Code abgearbeitet wird.
Und entgegen FeFe, sollten Usernamen tatsächlich eine bestimmte Länge nicht überschreiten. Aber das ist eben nur ein Teil der Validierung.
Gruss Michael
Lieber Michael_K,
danke für den Post. Aber ganz ehrlich, das Einzige, was man dem kleinen Code-Schnipsel vorwerfen kann, ist die fehlende Information/inline Kommentierung, was welche Variable beinhaltet bzw. bezweckt. Und FeFe schreibt auch viel Unsinn, wenn der Tag lang ist.
für mich war Fefe der Erstkontakt mit diesem Code. Das dortige Beispiel war für mich nur ein Aufhänger und Ideengeber, um einen kurzen Beitrag für Hobbyisten in diesem Forum zu schreiben.
Ob sich Fefe hier berechtigterweise amüsiert oder nicht, oder ob ein Backend von Security-Software überhaupt mit PHP realisiert werden sollte, ist nicht mein Anliegen.
Liebe Grüße
Felix Riesterer
Moin,
und sollte man nicht besser auch die mb_Funktionen nutzen für den Stringvergleich, bzw. vorher ausschließen. dass es sich um andere Codierungen als 8-Bit handeln kann?
Grüße aus Düsseldorf
M.
Hallo mitleserin,
angesichts der Tatsache, dass mit Locales hantiert wird: ja, auf jeden Fall. Das ist wesentlich gravierender als das Umkopieren von $_POST-Feldern in lokale Variablen, ohne zu validieren und ohne zu prüfen, ob der $_POST-Eintrag überhaupt da ist.
In Sprachen mit "klassischem lateinischem" Alphabet sind nur wenige Codepoints dabei, die außerhalb des ASCII-Zeichensatzes liegen. Beispielsweise ein Russe oder Grieche würde sich aber schon wundern, warum sein Name nur 31 Zeichen lang sein darf…
Rolf
Liebe mitleserin,
und sollte man nicht besser auch die mb_Funktionen nutzen für den Stringvergleich,
welche? Und wo in meinem Beispiel würdest Du die einsetzen? In user_exists
? Warum genügt da kein simpler Vergleich zweier (String-)Variablen?
bzw. vorher ausschließen. dass es sich um andere Codierungen als 8-Bit handeln kann?
Warum sollte das wichtig sein?
$users = ['a-a-A-A', 'o-o-O-O', 'u-u-U-U', 's-S-s'];
$test = ['a-ä-A-Ä', 'o-ö-O-Ö', 'u-ü-U-Ü', 's-S-ß'];
foreach ($test as $login) {
if (in_array($login, $users)) {
echo '„',$login,'“ ist gültig<br>';
} else {
echo '„',$login,'“ gilt NICHT<br>';
}
}
// Ergebnisse:
//„a-ä-A-Ä“ gilt NICHT
//„o-ö-O-Ö“ gilt NICHT
//„u-ü-U-Ü“ gilt NICHT
//„s-S-ß“ gilt NICHT
Liebe Grüße
Felix Riesterer
Hallo Felix,
Warum sollte das wichtig sein?
Weil strlen() nur dann richtig zählt. Dass du keine Längentests mehr machst, heißt nicht, dass sie nicht in anderen Prüfungen nötig sein könnten.
Rolf