Zwei Jahre (und ein bisschen) neunzehn83.de

Party Hard

Wie auch schon letztes Jahr habe ich den Geburtstag (oder sagt man Jahrestag) meines Blogs knapp verpennt. Macht aber nichts, zu bloggen hab ich nämlich auch vergessen. 2011 waren die Anspüche groß, die Resultate blieben aber wie so oft weit hinter den Erwartungen zurück. So habe ich es in einem Jahr auf gerade einmal 16 Beiträge gebracht. Pfui!

2012 wird aber alles besser. Gebloggt wird ab sofort übrigens mit WordPress 3 und markdown (yay). Ideen für neue Beiträge habe ich einige, mitunter sind das echte Kracher, also dranbleiben, denn die kalte Jahreszeit kommt bestimmt.

SVG nach PNG umwandeln mit Textpath [Debian Linux]

Kurz erwähnt, da es mich selbst zu viel Zeit gekostet hat dieses eigentlich einfache Problem zu lösen:

Möchte man ein SVG in PNG (oder sonstige Pixelgrafikformate) umwandeln, versucht man das wohl zunächst mit ImageMagick. Das funktioniert solange auch prima, bis im SVG ein <textpath> vorkommt. Dieser verschwindet nämlich bei der Umwandlung in ein PNG mit imagick. Selbes Problem tritt mit meiner zweiten Wahl, dem Tool rsvg (librsvg2-bin), auf.

Mit inkscape funktioniert die Umwandlung wie gewünscht. Es ist mit Sicherheit weniger verbreitet wie convert (imagick), aber immerhin ist es als Debian-Paket (“inkscape”) verfügbar. Die Windowsversion ist im Übrigen ebenfalls einen Blick wert: ein wirklich brauchbares, quelloffenes Vektorgrafikprogramm!

PHP-Security! Heute: path traversal

Path traversal bzw. directory traversal ist eine Methode, um aus vorgesehenen Verzeichnissen auszubrechen. In Bezug auf PHP findet diese Sicherheitslücke  Anwendung, da Dateien oftmals anhand des Querystrings eingebunden werden. Beispiel:

http://example.org/index.php?site=impressum.php
<?php
    include './includes/sites/' . $_GET['site'];
?>
Das ist natürlich fatal. Mittels ../ in $_GET['site'] kann man das vorgegebene Verzeichnis verlassen und je nach Rechten auch sicherheitsrelevante Dateien ausgeben lassen. Denn: wird eine nicht-PHP-Datei mittels include eingebunden wird deren Inhalt 1:1 an den Browser gesendet.

Oftmals wird das Ausbrechen aus einem Verzeichnis dadurch versucht zu verhindern, indem ‘../’ aus $_GET['site'] entfernt wird.

$save = str_replace('../', '', $_GET['site']);
Auch ganz schlecht: Aus einem ....// würde ../, da str_replace bereits ersetzte Teile nicht nochmals ersetzt. Außerdem kommen durch URL-Encoding und unterschiedliche Directory-Separator (Linux/Windows) noch weitere Zeichen in Frage, die es erlauben aus einem Verzeichnis auszubrechen.

Sehr verbreitet ist auch die Dateiendung vorzugeben:

include './includes/sites/' . $_GET['site'] . '.php';
Je nach Betriebssystem ist hier mit einem NULL-Byte (Url-Encoded: %00) die vorzeitige Terminierung des Strings möglich:
http://example.org/index.php?site=../../../../../../../etc/passwd%00
Hier muss man übrigens nicht die exakte Anzahl der nach oben zu gehenden Verzeichnisse wissen – ist man im Root-Verzeichnis angelangt, können beliebige ../ ohne Auswirkungen folgen.

Mit aktiviertem allow_url_fopen gibt es noch weitere Zeichen zu prüfen – das ist dann aber kein path traversal mehr und somit nicht bestandteil dieses Blog-Beitrages.

Was tun?

Statt str_replace sollte man besser preg_replace mit einem Pattern wie #\.+[/\]+# verwenden. Noch besser ist es natürlich von vorne herein eine Whitelist zu führen – also ein Array mit erlaubten Dateinamen und dann prüfen, ob $_GET['site'] in diesem Array vorkommt. Eine weitere Methode ist, den kanonischen, absoluten Pfad mittels realpath() zu erzeugen und dann den Beginn dieses Pfades mit dem Document-Root (bzw. dem erlaubten Verzeichnis) abzugleichen.

Variablen tauschen ohne Hilfsvariable?

Pah!

list($a, $b) = array($b, $a);

Über Primary-Keys in URLs .. und sonstwo

Ein Primary-Key identifiziert eindeutig ein Tupel (Zeile/Reihe/Row) in einer relationalen Datenbank. Im trivialen Fall ist das eine Integer-Spalte mit auto_increment – wenn wir von MySQL reden, wie so oft hier. Eine fortlaufende Nummer also. Diese Nummer wird in Webanwendungen oft direkt an den User weitergegeben. Zum Beispiel als Teil der URL oder als Datensatznummer (z.B. Bestellnummer in einem Online-Shop).

Beispiele

1. Online-Shop: $kunde erhält seine Bestellbestätigung per E-Mai mit der Bestellnummer 27.

Das wäre mir (als Shop-Betreiber) ein Dorn im Auge, da es Rückschlüsse auf die Anzahl der bisherigen Bestellungen zulässt. Das lässt sich noch relativ leicht beheben, indem man die auto_increment-Spalte nicht bei 1 sondern bei einer höheren Zahl beginnen lässt (ALTER TABLE tbl AUTO_INCREMENT = 1000;). Doch was, wenn der Kunde zwei Wochen später erneut bestellt, und eine nur wenig höhere Bestellnummer erhält? Dies lässt wiederum Rückschlüsse auf die Häufigkeit von Bestellungen im Shop zu. Daran ändert auch der Beginn bei höheren Zahlen nichts.

2. URL: http://domain.com/user/123

Wir nehmen an, hinter dieser URL verbirgt sich ein öffentliches Benutzerprofil. Auch hier selbes Problem wie oben (Aussage über Quantität). Zudem kann man hier, aufgrund der fortlaufenden Nummer, die “Umgebung erkunden”, andere Benutzerprofile erraten oder per Script sämtliche Userprofile crawlen (for i=1; i<999;i++). Hier bräuchte man also im Idealfall eine zufällig fortlaufende Zahlen/Nummernkombination, mit genügend “Lücken” um das direkte erraten anderer Profile zu verhindern erschweren. Dennoch sollte die User-ID natürlich so kurz wie möglich sein.

Das dürfte ja alles soweit bekannt sein. In den meisten Szenarien spielt das auch überhaupt keine Rolle. In einem Blog oder Forum z.B. dürfen die Post-IDs gerne den Primary-Keys entsprechen. Was aber, wenn der paranoide Webmaster Rückschlüsse nicht (oder sagen wir besser: nur erschwert) zulassen will?

Anforderungen an eine ID-Verschleierung

  • wenig Overhead (nicht länger als nötig)
  • leicht reversibel (ohne Datenbankabfrage, ohne Speicherung der verschleierten ID in extra DB-Spalte)
  • Erhöhung des PK um 1 soll große Änderung der verschleierten ID bewirken
  • kollisionsfrei, zu 100%
  • optional: Spielraum/Lücken um das Erraten von weiteren IDs zu erschweren

Mögliche Lösungen zum Verbergen der ID

GUID

GUIDs haben mit Sicherheit Ihre Daseinsberechtigung. In den meisten Fällen sind sie aber einfach nur des Guten zu viel. Von der Performance auf Datenbankseite möchte ich jetzt gar nicht reden. Auch nicht über die Tatsache, dass GUID in URLs schrecklich aussehen. Das Ziel muss sein, so wenig wie möglich Overhead zu erzeugen. GUID: no-go!

Hashen des PKs

Ein Hash resultiert immer in einem String mit fester Länge. Selbst bei sehr kleinen Primary-Keys, wäre der Hash immer gleich lang. Welch’ Verschwendung! Außerdem sind Hashs nicht kollisionsfrei*, und sie lassen sich auch nicht zurückrechnen. D.h. man müsste den Hash zusätzlich zum Primary-Key speichern (noch mehr Overhead).

[* zugegeben, in diesem Ganzzahlbereich um den es hier geht wohl schon]

Basis

Man kann den Primary-Key einfach in einer anderen Basis darstellen, Base32 od. 64 zum Beispiel. Wenn man darauf besteht, dass die ID nach wie vor aus Zahlen bestehen soll, hat man Pech. Auch doof ist, dass eine Erhöhung des PK um 1 nur eine sehr kleine Änderung der verschleierten ID bewirkt. Beispiel: 12345(base10) = C1P (base32), 12346(base10) = C1Q(base32)).

Random-ID

Die Idee dahinter ist, im Vorfeld einen Pool an Random-IDs zu erzeugen (in einer extra DB-Tabelle). Ein UNIQUE-Key verhindert Duplikate. Beim Anlegen eines neuen Datensatzes, wird aus der Kandidaten-Tabelle dann eine Zufallszahl geholt und mit dem PK verknüpft. Das Ganze geht auch ohne vorheriges Erstellen der Kandidaten-Tabelle. Endlos-Schleife und race conditions nicht ausgeschlossen.

ID Verschleiern

Alles “Bekannte” scheint hier nicht zum Erfolg zu führen. Es gibt unendlich viele Methoden, durch logische Verknüpfung (XOR in erster Linie) und sonstige mathematische Funktionen, Zahlen zu verschleiern. Eine einfache, auf diese Problematik passende, möchte ich hier vorstellen.

Man nehme einmalig eine zufällige Zahl (“secret”), die mindestens so groß ist, wie die höchste Primary ID. Der längste PK kann, wenn wir ein MySQL SMALLINT unsigned annehmen, max. 65535 sein. Unsere Zufallszahl: 99565. Diese Zahl ist systemweit immer gleich – wird also für alle späteren Ver- und Entschleierungen verwendet.

Das eigentliche Prozedere ist einfach: Die binäre Präsentation des PK wird umgekehrt und mit der Zufallszahl mit XOR verknüpft. Von der Zufallszahl verwenden wir nur so viele binäre Stellen, wie unser PK hat. Da durch das Umkehren und XORen führende Nullen entstehen können, stellen wir immer eine binäre 1 dem  umgekehrten Binärstring voran – sonst kommt es zu Kollisionen. Beispiel: PK 12 = 1100 Flip +1 = 10011 99565 = _1100[0010011101101] (das ist unser Secret) XOR = 11111 = 31

PK 13      =  1101
Flip +1    = 11011
99565      = _1100[0010011101101] das ist unser Secret)
XOR        = 10111 = 23

PHP-Code

function obfuscateID($pk, $secret, $margin = 5) {
    if ($margin) {
        $pk = base_convert($pk, 10, 10-$margin);
    }

    $pk = bindec(1 . strrev(decbin($pk)));
    $pk ^= bindec(substr(decbin($secret), 0, strlen(decbin($pk))-1));

    return $pk;
}

function deObfuscateID($pk, $secret, $margin = 5) {
    $pk ^= bindec(substr(decbin($secret), 0, strlen(decbin($pk))-1));
    $pk = bindec(substr(strrev(decbin($pk)), 0, -1));

    if ($margin) {
        $pk = base_convert($pk, 10-$margin, 10);
    }

    return $pk;
}

Hier mit Highlighting.

Vorsicht mit bindec() und decbin() auf 32bit-Systemen: hier sind max. Umwandlungen bis zu 4,294,967,295 möglich. Wer auf 32bit mehr braucht, muss sich diese Funktionen auf Stringbasis selber basteln. Anregung dazu findet man wie so oft in den Kommentaren des PHP-Manuals.

Sicherheitsmarge

Um das Erraten von Nachbarn zu erschweren, können die Lücken zwischen den PKs vergrößert werden. Das passiert mit dem Parameter $margin. Dieser muss ebenso wie das $secret systemweit immer gleich sein. Beim Zurückwandeln muss also das selbe Margin angegeben werden, wie für die Verschleierung. $margin kann ein Wert zwischen 0 und 9 haben und basiert auf einer simplen Basisumwandlung (umgerechnet wird von Basis 10 in Basis 10-$margin).

Ausgabe

Bis hierhin war’s ganz schön trocken. Höchste Zeit für ein wenig Praxis! Der folgende PHP-Schnipsel in Kombination mit den obigen Funktionen..

$secret = 99565;
for ($i = 1; $i &lt; 1000; $i++) {
    $camo = obfuscateID($i, $secret);  
    echo "$i: $camo <br />";
}

ergibt folgende Ausgabe:

[..]
524: 7960
525: 15439
526: 11343
527: 13391
528: 9295
[..]
991: 19678
992: 29406
993: 21214
994: 25310
995: 30430
[..]
Äußerst nett, wie ich finde!

Zahlen oder Alphanum?

Um Platz zu sparen, könnte man die nach wie vor aus Zahlen bestehende, verschleierte ID in eine höhere Basis (32, 64) umwandeln. Auch mit dieser Umwandlung kann man relativ einfach für (weitere) Lücken sorgen. Da dies aber gefühlt ohnehin schon mein längster Blogbeitrag ist (in jedem Fall was die investierte Zeit angeht), lassen wir das mal so offen stehen bzw. überlasse ich es dem Leser als Übung :)