Ich möchte nochmal vor selbst-gestrickten Lösungen warnen und für die Komplexität von sicherheits-kritischen Systemen sensibilisieren. Man sollte nicht zu leichtfertig an solche Systeme heran gehen. Das verlinkte Login-System ist ein Beispiel für einen mutigen, aber dennoch naiven Ansatz. Das habe ich früher schon kritisiert, und wurde im Gegenzug dafür kritisiert nicht konkret genug geworden sein. Die Kritik nehme ich mir zu Herzen und versuche diesmal alle meine Punkte mit Quellen und Zitaten zu belegen. Hauptsächlich beziehe ich mich dabei auf die Cheat Sheets von OWASP und die Paragon Initiative. Gegliedert habe ich die Kritik nach fachlichen Merkmalen, zu jeder Überschrift gibt es ein passendes Cheat Sheet von OWASP, das ich am Beginn jedes Abschnitts verlinke. Die OWASP Cheat Sheets sind ein guter Ausgangspunkt, allerdings keine erschöpfende Referenz für alle sicherheits-relevanten Software-Aspekte.
Authentication
Authentication Cheat Sheet
Das Login-System benutzt die selben Konfigurations-Dateien, wie der Apache-Webserver für die Benutzer-Authentifizierung. Das habe ich zum ersten mal hier angesprochen, habe es aber nicht weiter unterfüttert. Das bringt allerlei Probleme mit sich, wenn man das nicht explizit durch die Apache-Konfiguration unterbindet. Zum Beispiel kann das dazu führen, dass ein Nutzer mehrfach nach seinem Passwort gefragt wird: Einmal durch den Webserver und einmal durch das darauf laufende PHP-Skript. Das ist unkomfortabel, allerdings auch ein Sicherheits-Risiko, weil durch das Basic-Auth-Verfahren nun bei jeder Folgeabfrage Benutzername und Passwort übertragen werden. Außerdem erfordert es erhöhten Konfigurations-Aufwand, wenn man Basic-Auth und PHP-Login von einander trennen möchte. Das Cheat Sheet schreibt dazu diesen Punkt:
Do NOT allow login with sensitive accounts (i.e. accounts that can be used internally within the solution such as to a back-end / middle-ware / DB) to any front end user interface. - OWASP
Ferner ist das Login-Skript mit einer minimalen Passwort-Länge von 8 Zeichen vorkonfiguriert. Diese Länge gilt inzwischen nicht mehr als hinreichend sicher und sollte auf 10 angehoben werden. Aus dem Cheat Sheet:
Minimum length of the passwords should be enforced by the application. Passwords shorter than 10 characters are considered to be weak (NIST SP800-132). - OWASP
Das Skript funktioniert sowohl mit HTTPS als auch mit unverschlüsseltem HTTP. Das ist prinzipiell in Ordnung, wenn im Falle von HTTP der Transport irgendwie anders abgesichert wird, zum Beispiel durch SSH-Forwarding oder einen VPN-Tunnel. Das muss beim Aufsetzen des Skriptes Berücksichtigung finden. Es gibt heute allerdings auch keine guten Gründe mehr dafür, überhaupt auf HTTPS zu verzichten. Aus dem Cheat Sheet:
The login page and all subsequent authenticated pages must be exclusively accessed over TLS or other strong transport. - OWASP
Und in dem Session Management Cheat Sheet wird dieser Punkt ebenfalls nochmal betont.
In order to protect the session ID exchange from active eavesdropping and passive disclosure in the network traffic, it is mandatory to use an encrypted HTTPS (SSL/TLS) connection for the entire web session, not only for the authentication process where the user credentials are exchanged. - OWASP
Wenn ein Login-Versuch fehlschlägt, antwortet das Skript mit einer präzisen Fehlerbeschreibung, die erklärt ob das Passwort falsch war oder der Benutzername nicht existiert. Das ist aus Benutzersicht erstmal bequem, kann aber auch von Angreifern genutzt werden, um zu herauszufinden ob ein Benutzer in deinem System registriert ist. Zum Beispiel, könnte ich einfach mal die Email-Adresse meines Partners oder meiner Kollegen ausprobieren. Stell dir bspw. vor, das mache ich in einem AIDS-Hilfe-Forum, einer Abtreibungs-Klinik oder einem Forum für trockene Alkoholiker. Aus dem Cheat Sheet:
An application should respond with a generic error message regardless of whether the user ID or password was incorrect. It should also give no indication to the status of an existing account. - OWASP
Session Management
Session Management Cheat Sheet
Der Name des Session-Cookies des Skripts lautet einfach PHPSESSID
, dadurch weiß ein Angreifer, dass PHP eingesetzt wird und kann gezielt PHP-Sicherheitslücken ausprobieren. Besonders heikel ist das im Zusammenhang mit veralteten PHP-Versionen, die keine Sicherheitspatches mehr erhalten. Dazu später noch mehr. Aus dem Cheat Sheet:
The session ID names used by the most common web application development frameworks can be easily fingerprinted, such as PHPSESSID (PHP), JSESSIONID (J2EE), CFID & CFTOKEN (ColdFusion), ASP.NET_SessionId (ASP .NET), etc. Therefore, the session ID name can disclose the technologies and programming languages used by the web application. - OWASP
Um den Fall zu vermeiden, dass ein Session-Cookie über eine unverschlüsselte Verbindung übertragen wird, sollte die Session mit der Option 'cookie_secure' => true
gestartet werden, das ist aktuell nicht der Fall. Aus dem Cheat Sheet.
The Secure cookie attribute instructs web browsers to only send the cookie through an encrypted HTTPS (SSL/TLS) connection. This session protection mechanism is mandatory to prevent the disclosure of the session ID through MitM (Man-in-the-Middle) attacks. It ensures that an attacker cannot simply capture the session ID from web browser traffic. - OWASP
Weiterhin sollte beim Session-Start die Option 'cookie_httponly' => true
gesetzt werden, damit das Cookie nicht von JavaScript ausgelesen werden kann. Das ist bisher leider auch nicht der Fall. Aus dem Cheat Sheet:
The HttpOnly cookie attribute instructs web browsers not to allow scripts (e.g. JavaScript or VBscript) an ability to access the cookies via the DOM document.cookie object. This session ID protection is mandatory to prevent session ID stealing through XSS attacks. - OWASP
Und von der Paragon Initiative heißt es ganz ähnlich:
In a similar vein, if you're using PHP's built-in session management features (which are recommended), you probably want to invoke session_start() like so:
session_start([
'cookie_httponly' => true,
'cookie_secure' => true
]);
This forces your app to use the HTTP-Only and Secure flags when sending the session identifier cookie, which prevents a successful XSS attack from stealing users' cookies and forces them to only be sent over HTTPS, respectively.
Wenn das Skript über HTTP läuft, dann wir die Session-ID bei jeder Anfrage erneuert. Das macht asynchrone Anfragen zu einem Glücksspiel, weil die asynchronen Anfragen dann einer Race-Condition unterliegen. Wenn es einen Angreifer gibt, der eine Session-ID mitgelesen hat, kann diese Race-Condition für sich nutzen, um den eigentlichen Eigentümer der Session auszusperren.
PHP Configuration
PHP Configuration Cheat Sheet
Auf der Seite wird das Skript unter anderem damit vermarktet, dass es ab PHP 5.3 funktioniert. PHP 5.3 erhält aber seit Jahren keine Sicherheits-Patches mehr. Eine Kette ist nur so stark wie ihr schwächstes Glied. Von der Paragon-Initiative:
PHP 7.2 was released on November 30, 2017.
As of this writing, only PHP 7.1 and 7.2 receive active support by the developers of the PHP programming language, while PHP 5.6 and 7.0 both receive security patches for approximately another year.
Some operating systems provide long-term support for otherwise unsupported versions of PHP, but this practice is generally considered harmful. In particular, their bad habit of backporting security patches without incrementing version numbers makes it hard to reason about the security of a system given only its PHP version.
Consequently, regardless of what promises another vendor makes, you should always strive to run only actively supported of PHP at any given time, if you can help it. That way, even if you end up on a security-only version for a while, a consistent effort to upgrade will keep your life free of unpleasant surprises.
PHP 7.1 erhält ab Dezember diesen Jahres keine Sicherheitspatches mehr. Also sollte man schonmal auf 7.2 oder. 7.3 umsteigen.
Password Storage
Password Storage Cheat Sheet
Für die PHP 5.3 Kompatibilität gibt es in dem Skript einen eigenen Salt-Generator, der ist allerdings kryptografisch nicht sicher.
Aus dem Cheat Sheet:
Follow these practices to properly implement credential-specific salts: [...]
- Use cryptographically-strong random data;
Im Code wird die PHP-Funktion rand
dafür benutzt. Im PHP-Handbuch wird vor dem Einsatz dieser Funktion für kryptografisch sichere Zufallszahlen gewarnt:
This function does not generate cryptographically secure values, and should not be used for cryptographic purposes. If you need a cryptographically secure value, consider using random_int(), random_bytes(), or openssl_random_pseudo_bytes() instead.