Sichere suid-Dateizugriffe programmieren

25/10/2007 - 22:26 von Nikolaus Schulz | Report spam
Hallo,

bei meinen Bemühungen, Symlink-Angriffe und ihre Vermeidung zu
verstehen, hab ich mir mal das folgende Szenario vorgenommen: ein
Programm làuft als root und soll User-Dateien verarbeiten: lesen und
schreiben, neue Dateien anlegen, also volles Programm.

Die Pfade zu den Dateien seien (zunàchst mal) von root vorgegeben.
Dennoch kann der betreffende User natürlich die Pfade nach Belieben per
Symlink umbiegen. Wie behandelt man das am geschicktesten?

Lassen sich den Pfaden statisch User zuordnen, wàre eine relativ
einfache Möglichkeit, diese Zuordnung per Konfiguration/Option im
root-Programm festzulegen. Das Programm könnte dann vor jeglichen
Dateizugriffen mit seteuid() die jeweilige uid annehmen, und gut.
Richtig?

Eine zweite Idee ist, für jede einzelne Datei den Owner zu checken und
ein setuid(owner-uid) auszuführen. Hier kommen nun Symlink-Angriffe
ins Spiel: ein einfaches open() kann per Symlink überallhin geschickt
werden. Wie làßt sich nun vermeiden, daß ein User so das root-Programmm
auf die Dateien eines Dritten ansetzt und fàlschlich diese verarbeitet
werden?

Gibt's noch andere Möglichkeiten? Ich bin auch für ganz allgemeine
Kommentare oder Hinweise auf gutes Informationsmaterial dankbar.
Meine (ausgiebige) Recherche im Internet ist nicht so richtig
befriedigend verlaufen. Da habe ich z.B. noch nicht mal eine
Beschreibung gefunden, was *genau* eigentlich der Dentry '..' ist und
welchen Gesetzen er unterworfen ist.

Ich bin nicht gerade besonders firm, was die "dunklen Ecken" von
Unix-Dateisystemen betrifft, bin da bisher eher schlichter User
gewesen... Ich hab aber mal für den zweiten Ansatz oben den Versuch
einer Lösung in Python programmiert, und bitte nun um eure nachsichtigen
Kommentare. :-) Der Code ist "blind" geschrieben und nicht getestet,
es geht mir mehr ums Prinzip. Vorsicht, ist nicht ganz kurz/trivial.
Ach ja, falls das jemand gut finden sollte, steht's unter GPL >=2. :-p
Also los:

<python>
def verify_user(filepath):
"""Return the owner of of the given file. If the path is not
secure, bail. If an (untrusted) user's symlink sends us to another
user's files, bail. Group-writable non-symlink path components are
considered secure iff they are owned by root."""
# Walk down the path, beginning at cwd. Resolve symlinks at every path
# component, check permissions and owner. All components must be owned by
# root or {the current,one single} user. If running as root, drop privileges
# as soon as we hit the first path component not owned by root.
import os
from stat import *
curdir = None
try:
curdir = os.open(".", os.O_RDONLY)
if filepath[0] == '/':
os.chdir("/")
relpath = filepath.lstrip("/")
else:
relpath = filepath
cur_euid = os.geteuid()
if cur_euid == 0:
good_user = None # yet to be determined
else:
good_user = cur_euid
fd = None
lncount = 0
while True:
if lncount >= 100: raise UnexpectedError("ELOOP")
relpath = relpath.strip("/")
try:
head, tail = relpath.split("/", 1)
except ValueError:
head = relpath
tail = None
assert(head)
lst = os.lstat(head)
owner = lst[ST_UID]
if owner != 0:
badperms = S_IWGRP|S_IWOTH
if not good_user:
# Entering user-controlled subtree.
good_user = owner
if cur_euid == 0:
# Drop privileges.
os.seteuid(good_user)
cur_euid = good_user
elif owner != good_user:
raise UnexpectedError("unsecure path '%s':"
"multiple file owners" % filepath)
else:
badperms = S_IWOTH
if S_ISLNK(lst[ST_MODE]):
lncount = lncount + 1
ltarget = os.readlink(head)
if ltarget[0] == "/":
os.chdir("/")
relpath = ltarget.lstrip("/")
else:
relpath = ltarget + "/" + tail
continue
fd = os.open(head, os.O_RDONLY)
# Note, if we should want to return this descriptor for later
# use, we would need to open it read-write.
fst = os.fstat(fd)
if not os.path.samestat(lst, fst):
# fishy
raise UnexpectedError("file changed while reading: '%s'" %
os.path.abspath(head))
if fst[ST_MODE] & badperms:
raise UnexpectedError("unsafe permissions on %s" %
os.abspath(head))
if not S_ISDIR(fst[ST_MODE]):
if tail:
# Haven't resolved entire path yet
raise UnexpectedError("ENOTDIR")
if S_ISREG(fs[ST_MODE]):
break
raise UnexpectedError("not a regular file: '%'" %
os.path.abspath(head))
os.fchdir(fd)
os.close(fd)
fd = None
relpath = tail
finally:
if fd != None:
os.close(fd)
if curdir != None:
os.fchdir(curdir)
os.close(curdir)
# Hm, probably I should return the final 'dirname(mbox)' fd,
# or the mbox fd, or both.
return good_user # None if owner is root.
</python>

Was haltet ihr davon? Auch sowas Profanes wie Hinweise auf
Programmierfehler ist willkommen. :-)

Nikolaus
 

Lesen sie die antworten

#1 Rainer Weikusat
26/10/2007 - 11:11 | Warnen spam
Nikolaus Schulz writes:
bei meinen Bemühungen, Symlink-Angriffe und ihre Vermeidung zu
verstehen, hab ich mir mal das folgende Szenario vorgenommen: ein
Programm làuft als root und soll User-Dateien verarbeiten: lesen und
schreiben, neue Dateien anlegen, also volles Programm.

Die Pfade zu den Dateien seien (zunàchst mal) von root vorgegeben.
Dennoch kann der betreffende User natürlich die Pfade nach Belieben per
Symlink umbiegen. Wie behandelt man das am geschicktesten?



In dem man vor dem Dateizugriff die euid 'auf eine geeignete Weise'
auf die uid des Benutzers, als der das Programm erscheinen soll,
setzt.

[...]

<python>
def verify_user(filepath):



["Das Grauen" (c) J. Conrad ]

return good_user # None if owner is root.
</python>

Was haltet ihr davon?



Nichts. G. van Rossum hat im Zusammenhang mit 'The Right Way 2.0'
angemerkt, dass er map & friends aus der Sprache eliminiert haette,
weil man stattdessen ja auch Schleifen benutzen koenne und somit die
Notwendigkeit, 'small functions' zu schreiben, entfiele. Man kann das
so verstehen, dass es eine schlechte Idee ist, in Python 'small
functions' zu schreiben, oder, anders ausgedrueckt, das die Sprache
sich fuer strukturierte Programmierung nicht eignet, was noch nicht
mal 'Mr The Right Way' als nachteilig zu empfinden scheint.

Der oa "let's jump in circles"-Salat ist ein hervorragendes Beispiel,
dafuerm, dass man, um Leute am spaghettikodieren zu hindern, nicht das
'goto-Statement', sondern jegliches Verzweigungskonstrukt eliminieren
muss. Wenn ich soetwas in Code, den ich zu 'maintainen' haette,
vorfaende, wuerde ich es vermutlich schlicht loeschen.

Ähnliche fragen