Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Ruby im Tresor sichern



Walter Webcoder hat eine tolle Idee für ein Webportal: Die Arithmetik-Seite. Umgeben von allerlei coolen mathematischen Links und einem Haufen Bannern, die ihn reich machen werden, befindet sich in der Mitte ein einfacher Rahmen mit einem Text-Feld und einem Button. Der Benutzer tippt einen arithmetrischen Ausdruck in dieses Feld, drückt den Button und dann wird das Ergebnis angezeigt. Alle Rechner auf der ganzen Welt werden über Nacht überflüssig und Walter kassiert und beschränkt sich in Zukunft auf das Sammeln von Auto-Nummernschildern (daran sieht man, dass Walter aus den USA kommt, wo sonst sammelt man Auto-Nummernschilder, der Übersetzer).

Das Implementieren des Rechners ist ganz einfach, denkt Walter. Er greift auf den Inhalt des Feldes mit Rubys CGI-Bibliothek zu und benutzt die eval-Methode, um den String als Ausdruck auszuwerten.

require 'cgi'

cgi = CGI::new("html4")

# Hol den Wert aus dem Feld "expression" expr = cgi["expression"].to_s

begin   result = eval(expr) rescue Exception => detail   # unkorrekte Ausdrücke abhandeln end

# Ergebnis für den Benutzer anzeigen...

Knapp sieben Sekunden, nachdem Walter seine Applikation ans Netz gehängt hat, tippt ein Zwölfjähriger aus Hintertupfingen mit Hormonproblemen und keinem echten eigenen Leben ``system("rm *")'' in das Feld und, zusammen mit seiner Applikation, fallen Walters hochfliegende Träume in sich zusammen.

Walter lernte eine wichtige Lektion: Alle externen Daten sind gefährlich. Lass sie niemals in die Nähe eines Interfaces, das dein System ändern könnte. In diesem Fall waren der Inhalt des Feldes die externen Daten und der Aufruf von eval war das Sicherheitsleck.

Glücklicherweise bietet Ruby die Möglichkeit, dieses Risiko zu verringern. Alle Informationen von draußen können als tainted [Tainted heißt etwa so viel wie verdorben oder vergiftet, der Übersetzer.] markiert werden. Wenn man dann in einem Safe-Modus arbeitet, erzeugen potenziell gefährliche Methoden einen SecurityError, wenn ihnen ein als tainted markiertes Objekt übergeben wird.

Safe-Levels

Die Variable $SAFE gibt an, wie paranoid Ruby sich verhält. Tabelle 20.1 auf Seite 261 zeigt, welche Checks auf den jeweiligen Leveln laufen.

$SAFE Einschränkungen
0 Es wird keine Kontrolle der extern übergebenen (tainted) Daten vorgenommen. Das ist der Default-Modus in Ruby.
>= 1 Ruby verbietet die Benutzung von tainted Daten durch potenziell gefährliche Operationen.
>= 2 Ruby verbietet das Laden von Programm-Dateien von global beschreibbaren Orten.
>= 3 Alle neu erzeugten Objekte werden als tainted angesehen.
>= 4 Ruby teilt das Programm gewissermaßen in zwei Teile auf. Nicht als tainted markierte Objekte können nicht mehr verändert werden. Üblicherweise benutzt man dies, um eine Sandbox zu erzeugen: Das Programm richtet eine Umgebung mit einem geringeren $SAFE-Level ein und setzt dann $SAFE auf 4, um zukünftige Änderungen an der Umgebung zu verhindern.

Normalerweise ist der Default-Wert von $SAFE null. Wenn allerdings ein Ruby-Script unter setuid oder setgid abläuft, [ Ein Unix-Script kann man so kennzeichnen, dass es unter einer vom tatsächlichen Benutzer abweichenden anderen Benutzer- oder Gruppen-Id läuft. Damit darf das Script Privilegien haben, die der Benutzer nicht hat; Das Script kann auf Resourcen zugreifen, die für den Benutzer sonst verboten sind. Diese Scripts werden setuid oder setgid genannt.] dann wird der Save-Level automatisch auf 1 gesetzt. Den Safe-Level kann man auch mit der Kommandozeilen-Option -T setzen oder über Zuweisung an $SAFE aus dem Programm heraus. Es ist aber nicht möglich, über eine Zuweisung den Wert von $SAFE wieder zu erniedrigen.

Der aktuelle Wert von $SAFE wird von jedem neu erzeugten Thread übernommen. Außerdem kann man innerhalb jedes Threads den Wert von $SAFE ändern, ohne dass die anderen Threads davon betroffen wären. Mit dieser Fähigkeit kann man sichere ``Sandboxen'' implementieren, in denen externer Code sicher laufen kann, ohne Risiko für den Rest der Applikation oder des Systems. Dazu wrappt man den aus einer Datei geladenen Code in seinem eigenen, anonymen Modul. Das schützt den Namensraum des übrigen Programms vor unerwünschten Veränderungen.

f=open(fileName,"w")
f.print ...   # schreibe nicht vertrauenswürdiges Programm in Datei
f.close
Thread.start {
  $SAFE = 4
  load(fileName, true)
}

Mit $SAFE auf Level 4 kann man nur noch gewrappte Dateien laden. Einzelheiten unter Kernel::load auf Seite 422.

Tainted Objekte

Jedes Ruby-Objekt, das aus einer externen Quelle kommt (zum Beispiel ein aus einer Datei gelesener String oder eine Umgebungsvariable), wird automatisch als tainted markiert. Wenn im Programm aus einem tainted Objekt ein neues Objekt abgeleitet wird, dann ist das neue genau so tainted, wie im Code unten gezeigt. Jedes Objekt, das irgenwo in seiner Vergangenheit externe Daten erhalten hat, ist tainted. Dieser Markierungs-Prozess läuft unabhängig vom aktuellen Safe-Level ab. Den Taint-Status eines Objekts kann man sich mit Object#tainted? ansehen .

>
# internal data
# =============
x1 = "a string"
x1.tainted? » false
x2 = x1[2, 4]
x2.tainted? » false
x1 =~ /([a-z])/ » 0
$1.tainted? » false

# external data
# =============
y1 = ENV["HOME"]
y1.tainted? » true
y2 = y1[2, 4]
y2.tainted? » true
y1 =~ /([a-z])/ » 1
$1.tainted? » true

Mit der taint-Methode kann man jedes Objekt zwangweise als tainted markieren. Falls der Safe-Level kleiner als 3 ist, kann man diese Taint-Markierung mit untaint auch wieder entfernen.[Es gibt auch abwegigere Möglichkeiten, dieses ohne untaint zu machen. Wir überlassen es der finsteren Seite eures Charakters, diese zu finden.] Dies ist natürlich nichts, was man leichfertig machen sollte.

Offensichtlich hätte Walter sein CGI-Script auf einem Safe-Level von 1 laufen lassen sollen. Dies hätte eine Exception ausgelöst, sowie das Programm versucht hätte, Daten nach eval zu schicken. Danach hätte Walter eine Reihe von Möglichkeiten gehabt. Er hätte einen passenden Parser für die Ausdrücke implementieren können und somit die Risiken der Benutzung von eval einfach umgangen. Wahrscheinlich hätte er aus Faulheit aber einfach nur einen Sicherheitscheck für die Daten gemacht und, falls sie harmlos ausgesehen hätten, dann die Markierung mit untaint entfernt.

require 'cgi';

$SAFE = 1

cgi = CGI::new("html4")

expr = cgi["field"].to_s

if expr =~ %r{^-+*/\d\seE.()*$}   expr.untaint   result = eval(expr)   # display result back to user... else   # display error message... end

Persönlich denken wir, dass Walter immer noch unnötige Risiken eingeht. Wir würden einen echten Parser hier vorziehen. Aber einen solchen zu implementieren, bringt uns nichts über das Tainting bei, also machen wir lieber weiter.

Und immer dran denken --- die Welt da draußen ist böse. Sei vorsichtig!

Definition der Safe-Levels
$SAFE >= 1
  • Die Umgebungs-Variablen RUBYLIB und RUBYOPT werden nicht ausgewertet und das aktuelle Verzeichnis wird nicht zum Pfad hinzugefügt.
  • Die Kommandozeilen-Optionen -e, -i, -I, -r, -s, -S und -x sind nicht erlaubt.
  • Man kann keine Prozesse aus $PATH starten, falls irgendein Verzeichnis darin global beschreibbar ist.
  • Man kann ein Verzeichnis, dessen Name ein tainted String ist, nicht manipulieren und auch kein chroot darauf ausführen.
  • Für tainted Strings kann man kein glob ausführen.
  • Für tainted Strings kann man kein eval ausführen.
  • Man kann keine Datei laden oder mit require einbinden, deren Name ein tainted String ist.
  • Keine Manipulation oder Abfrage auf eine Datei oder Pipe, deren Name ein tainted String ist.
  • Keine Ausführung eines System-Kommandos oder eines Programms über einen tainted String.
  • Man kann an trap keinen tainted String übergeben.

$SAFE >= 2

$SAFE >= 3
  • Alle Objekte werden als tainted erzeugt.
  • Man kann die Taint-Markierung nicht entfernen.

$SAFE >= 4
  • Man kann keine Arrays, Hashes oder Strings ohne taint verändern.
  • Keine Änderungen an globalen Variablen.
  • Kein Zugriff auf Instanz-Variablen ohne taint.
  • Keine Änderungen an Umgebungsvariablen.
  • Kein Schließen oder Wiederöffnen von Dateien ohne taint.
  • Kein Freeze für Objekte ohne taint.
  • Keine Änderungen an der Sichtbarkeit von Methoden (private/public/protected).
  • Kein Alias in einer Klasse oder einem Modul ohne taint.
  • Keine Meta-Informationen (wie etwa Methoden- oder Variablen-Listen).
  • Kein Definieren, Umdefinieren, Entfernen oder Undefinieren von Methoden in einer Klasse ohne taint oder einem Modul ohne taint.
  • Keine Änderung an Object.
  • Keine Entfernung von Instanz-Variablen oder Konstanten aus Objekten ohne taint.
  • Keine Manipulationen von Threads, kein Terminate für einen Tread außer dem aktuellen, kein Setzen von abort_on_exception.
  • Es darf keine tread-lokalen Variablen mehr geben.
  • Man kann keine exception in einem Thread mit einem geringeren $SAFE-Wert auslösen.
  • Man kann keine Threads in ThreadGroups bewegen.
  • Man kann nicht mehr exit, exit! oder abort aufrufen.
  • Man kann nur noch gewrappte Dateien laden, man kann Module nicht mehr in Klassen ohne taint oder Modulen ohne taint einbinden.
  • Man kann keine Symbol-Namen mehr in Objekt-Referenzen konvertieren.
  • Man kann nicht mehr in Dateien oder Pipes schreiben.
  • Man kann autoload nicht mehr benutzen.
  • Man kann keine Objekte mehr mit taint markieren.


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Jürgen Katins
Für das englische Original:
© 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0. That reference is available for download.
Diese Lizenz sowie das Original vom Herbst 2001 bilden die Grundlage der Übersetzung
Es wird darauf hingewiesen, dass sich die Lizenz des englischen Originals inzwischen geändert hat.
Für die deutsche Übersetzung:
© 2002 Jürgen Katins
Der Copyright-Eigner stellt folgende Lizenzen zur Verfügung:
Nicht-freie Lizenz:
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
Freie Lizenz:
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".