Archiv der Kategorie ‘Webtechnik‘

 
 

Ü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 :)

Data URIs vs. CSS-Sprites

CSS-Sprites sind eine elegante Möglichkeit um GET-Requests zu minimieren und daruch den Seitenaufbau zu beschleunigen. Dabei werden viele kleine Symbole in einer großen Grafik zusammengefasst und via CSS  immer nur der passende Ausschnitt der Grafik gezeigt. Eine andere Möglichkeit, GET-Requests zu sparen, besteht in der Verwendung des Data URI scheme.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSU[...]FTkSuQmCC" alt="" />

Statt einer URL wird  direkt der Dateiinhalt Base64-Kodiert angegeben. Das Bild wird also mit dem HTML-Quelltext mitgeladen. Es ist kein separater GET-Request notwendig. Das funktioniert selbstverständlich analog in CSS-Dateien  und selbstverständlich nicht mit dem IE6/7.

body { background: url('data:image/png;base64,iVBOR[..]QmCC'); }

Der fehlende IE6/7 Support mag ein Grund für die geringe Verbreitung des Data URI scheme sein. Erschwerend hinzu kommt die Tatsache, dass das Laden des (langen) Base64-Strings das weitere Rendering der Webseite zunächst blockiert. Die Base64-Darstellung ist im übrigen ca. 1/3 größer als die binäre und kann selbstverständlich nicht vom Browser gecached werden.

PHP-Skript im Hintergrund ausführen #2

Die bereits erwähnte Methode über Content-Length funktioniert zwar etwas hakelig, ist aber in Webspace-Umgebungen oft die einzige Methode ein PHP-Skript im Hintergrund auszuführen.

Eine andere Methode (neben dem Cronjob) besteht darin, über exec einen Hintergrundprozess zu starten. Beispiel (Linux only):

<?php exec('/usr/bin/php -f /var/www/cron.php > /dev/null &') ?>

Mit -f wird die auszuführende Datei angegeben. In Shared-Hosting-Umgebungen wird man keinen Zugriff auf die php-binary haben. Wget hingegen sollte verfügbar oder zumindest installierbar sein. Mit wget kann man prima “Cronjobs” über HTTP-GET im Hintergrund anstoßen:

<?php exec('wget -bq -o /dev/null -O /dev/null -t 1 http://www.example.org/cron.php') ?>

Wget’s Parameter sind case-sensitive. -b sorgt dafür, dass der wget-Prozess im Hintergrund ausgeführt wird, -q unterdrückt jegliche Ausgabe, -o leitet die heruntergeladene Datei nach /dev/null, -O schreibt das Logfile ebenfalls ins Nirvana und -t 1 unternimmt nur einen Versuch die folgende URL aufzurufen.

jQuerys $-Funktion

Wie wir alle wissen, werden Variablen in JavaScript nicht mit einem vorangestellten Dollarzeichen kenntlich gemacht. Insofern ist das Dollarzeichen für Javascript einfach nur das 36. Zeichen der ASCII-Tabelle. Und zudem ein gültiges Zeichen für Funktionsnamen (selbiges gilt neben allen Buchstaben und Zahlen auch für den Unterstrich).

function Framework(id) {
    return document.getElementById(id);
}
 
var $ = Framework;

Jetzt ist $() ein Alias für die Funktion Framework(). Benutzt werden kann das jetzt schon wie bei jQuery:

var nav = $('nav');

Um es Framework zu nennen, ist es wohl noch etwas dünn. Erweitern wir unser Framework also um eine handliche Datumsfunktion. Analog zu jQuery, gibt diese Funktion die aktuelle Zeit zurück.

Framework.now = function() {
    return (new Date).getTime();
}
 
$.now(); // 1301227135 (unix timestamp)

Das ist immer noch recht lasch! Weiter geht’s: Mit Javascripts Prototype Object ist es möglich einer Klasse eine gewisse Eigenschaft/Methode zu geben, die automatisch alle Instanzen dieses Objektes ebenfalls haben. Damit kann man z.B. jedem JS-Array eine each()-Methode verpassen, oder alle HTML-Elemente um Funktionen erweitern. Letzteres nehmen wir mal als Beispiel:

Element.prototype.addClass = function(className) {
    this.className += ' ' + className;
}
$('myDiv').addClass('foo');

Das Chaining von Methoden hat jQuery bekannt gemacht. Nichts leichter als das! Dazu muss die jeweilige Methode nur das Objekt selbst zurückgeben. Wir passen also unsere “addClass” Methode an und erstellen eine weitere um das Chaining zu testen.

Element.prototype.addClass = function(className) {
    this.className += ' ' + className;
    return this;
}
Element.prototype.content = function(content) {
    this.innerHTML = content;
    return this;
}
$('myDiv').addClass('foo').content('bar');

Das ist jetzt natürlich alles nicht elegant gelöst und bestimmt auch nicht Cross-Browser tauglich (ich rede mit dir, IE!), verdeutlicht aber was so prinzipiell hinter einen Javascript-Framework steckt. Hier nochmal am Stück und mit gehighlightetem hervorgehobenem Syntax.

Merke: jQuery ist nur ein JavaScript-Framework. Mit jQuery kann man also auf keinen Fall mehr Dinge tun, als mit reinem Javascript selbst. Mit jQuery geht das meist nur einfacher und vor allem konsistenter zwischen verschiedenen Browser-Versionen.

Protocol Relative URLs

Ist eine Webseite per https-Protokoll SSL-verschlüsselt aufrufbar, so müssen alle externen Objekte auf dieser Webseite ebenfalls über HTTPS geladen werden. Ist das nicht der Fall, gibt es im Internet-Explorer eine hässliche Meldung, bei anderen Browsern verschwindet einfach das begehrte Schloss-Icon.

Da Objekte (also Bilder, Scripte, CSS-Dateien usw.) meistens mit relativer Pfadangabe geladen werden, ist das aber kein Problem. Anders sieht das bei Benutzung eines CDN oder externen JS-Dateien, z.B. über Google APIs, aus. Hier muss nämlich die komplette URI zum Script angegeben werden, inklusive dem Protokoll. Das ist ein Problem, wenn die Seite sowohl über HTTP als auch HTTPS erreichbar ist. Abhilfe schafft das “network-path reference” Schema:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
In diesem Fall würde jQuery wahlweise per http oder https geladen werden – je nach Kontext.

Google-Analytics

Der Google-Analytics Code zum Einbauen in die eigene Webseite sieht so oder so ähnlich aus:

<script type="text/javascript">
   var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
   document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>

Hier wird je nach Protokoll per document.write ein script-Tag erzeugt. Echt unschön. Mit einer protocol relative URL sähe das so aus:

<script type="text/javascript" src="//google-analytics.com/ga.js"></script>

Viel schöner! Warum benutzt dann Google aber keine protokoll-relativen URLs? Schuld ist mal wieder, wie so oft, der IE. Und zwar in der Version 6 – hier soll es je nach Sicherheitseinstellungen zu Zertifikats-Fehlermeldungen kommen. Wen weder den IE6 noch seine Sicherheitseinstellungen interessieren, kann sein GA-Script also protokoll-relativ einbinden. Ach ja, wo wir schon beim Internet Explorer sind: In der Version 7 und 8 läd dieser protokoll-relative <link> und @import Ressourcen leider zwei mal herunter.