Vor einiger Zeit hatte ich das Vergnügen, die Entwicklung einer auf Python basierenden Applikation begleiten zu dürfen. Die Applikation machte, wie wohl viele andere auch, häufigen Gebrauch von der “Truthy” und “Falsy” Funktionalität von Python, um Sicherheitsentscheidungen treffen zu können. Da ich zur Zeit an der Vorbereitung für meine PCAP-Zertifizierung bin, wollte ich mir dieses Thema genauer unter die Lupe nehmen, um zu verstehen, ob es hier Situationen gibt in welchen diese Funktionalität zu heiklen Sicherheitsvorfällen führen können. Und wie sich herausstellte: Ja, es kann hier sehr wohl zu Problemen führen, wenn man implizites Vertrauen in die Funktionalität gibt. Aber zuerst, alles der Reihe nach.
Pythons “Truthy” und “Falsy” Funktionalität
Einleitend ist es wichtig zu verstehen, dass in Python alles ein Objekt ist und so (fast) alles auch Attribute und Methoden mit sich bringt. So sind auch Funktionen Objekte und bringen inhärent Methoden mit sich. Jedes Objekt in Python kann auf “Truth” value getestet werden. Wie die offizielle Dokumentation schreibt:
By default, an object is considered true unless its class defines either a
Python Documentation__bool__()
method that returnsFalse
or a__len__()
method that returns zero, when called with the object.
Auch an auf der Objekt-Definition (erinnere: Alles ist ein Objekt) wird in der Dokumentation nochmals folgendes definiert:
object.__bool__(self)
Python Documentation
Called to implement truth value testing and the built-in operationbool()
; should returnFalse
orTrue
. When this method is not defined,__len__()
is called, if it is defined, and the object is considered true if its result is nonzero. If a class defines neither__len__()
nor__bool__()
, all its instances are considered true.
myTrueValue = 1
myFalseValue = 0
if myTrueValue:
print("myTrueValue evaluated to True")
if myFalseValue:
print("myTrueValue evaluated to True")
>>> myTrueValue evaluated to True
Das Beispiel oben zeigt schön auf, dass die int() Klasse auf “True” evaluiert wird, wenn ein anderer Wert als “0” oder None gesetzt ist. Ich wollte es, gemäss des oben zitierten Python Dokumentation genauer wissen und sah mir die Implementationsdetails genaustens an. Die meisten Funktionen, wie auch das “Mutterobjekt” von wo aus alle Objekte vererbt werden, sind im Built-In Modul definiert. Wer auf die C-Ebene der Implementation möchte, kann dies hier tun. In diesem Modul werden base-klassen wie object, int, list usw. definiert. Und siehe da, die Klasse int wird wie folgt definiert:
class int:
@overload
def __new__(cls: type[Self], __x: str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc = ...) -> Self: ...
@overload
def __new__(cls: type[Self], __x: str | bytes | bytearray, base: SupportsIndex) -> Self: ...
if sys.version_info >= (3, 8):
def as_integer_ratio(self) -> tuple[int, Literal[1]]: ...
...
...
...
def __int__(self) -> int: ...
def __abs__(self) -> int: ...
def __bool__(self) -> bool: ... ### <--- HIER
Somit wird, wie in der Python Dokumentation erwähnt, die “callable” __bool__() Funktion definiert, welche ein True zurückgibt, wenn sie nicht leer ist. Interessant fand ich, dass in der object Klasse weder __bool__() noch __len() definiert sind, wie man hier sieht:
>>> import builtins
######## on object ########
>>> help(builtins.object.__bool__)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'object' has no attribute '__bool__'.
######## on int(): ########
>>> help(builtins.int.__bool__)
Help on wrapper_descriptor:
__bool__(self, /)
True if self else False
Die Evaluierung auf “True” oder “False” passiert auf Objekten jedoch trotzdem durch die in “if” und “while” liegende Funktionalität, alles zu evaluieren, auch wenn kein __bool__() oder __len()__ definiert wurde.
Die Gefahr des Impliziten Vertrauens
So weit, so gut. Eigentlich scheint die Python API sehr robust zu sein, zur Verwendung in sicherheitsrelevanten Situationen und das ist sie eigentlich auch. Eigentlich…
Lasst uns jedoch nun folgende Situation anschauen:
####### Class definitions #########
class Users(object):
def __init__(self, username, password, isAdmin=False):
self.username = username
self.password = password
self.isAdmin = bool(isAdmin)
def authorization(self):
'''return the boolean value of isAdmin to check, if user is admin'''
return self.isAdmin
##################################
#### Authorization Function ######
def authorize(user):
if user.authorization: ## <--- Implizites überprüfen ob user Admin ist
myCompanySecret = "This is strictly confidential information, only for admins"
return print(myCompanySecret + " accessed by: " + user.username + ", is Admin: " + str(user.isAdmin))
else:
print("You don't have access for the super confidential Information, accessed by: " + user.username + ", is Admin: " + str(user.isAdmin))
##################################
#### Instantiate User objects ####
normalUser = Users("Peter", "myAmazingPwd", isAdmin=False)
adminUser = Users("Manuel", "thisIsSecure1234", isAdmin=True)
#################################
##### Check Authorization #######
authorize(normalUser)
authorize(adminUser)
#################################
Nun, was wird wohl in if user.authorization: evaluiert? Schauen wir uns die Ausgabe an:
>>> This is a super confidential Information, only for Admins accessed by: Peter, is Admin: False
>>> This is a super confidential Information, only for Admins accessed by: Manuel, is Admin: True
Boom!! 💣🧨🤯 – Was ist passiert? Auch Peter, welcher klar kein Admin ist, erhielt Zugriff auf die geheimen Informationen. Dies geschah, weil implizites Vertrauen in die Evaluierung der Funktion user.authorization gegeben wurde. Wer genauer hinschaut, sieht, dass zusätzlich und fälschlicherweise nicht ein function-call durchgeführt wurde sondern das Funktions-Objekt evaluiert wurde. Und wie wir einleitend gesehen haben, wird jedes Objekt als “True” evaluiert, obwohl auf dem Objekt keine Funktion definiert wurde, um dies zu implementieren. Siehe hier:
print(dir(normalUser.authorization))
>>>['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__func__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
Kleiner Ausflug zu “callable”
Wie gesagt sind in Python alle Funktionen nichts anderes als Objekte. Um jedoch die in der Funktion definierte Funktionalität anzurufen und nicht das Objekt selbst zurückzuerhalten fügt man am Ende des Funktions-Objekts “()” an. Die “()” geben dem Compiler an, dass die definierte Funktion ausgeführt werden soll. Dies wird über die in jeder Funktion verfügbare ‘__call__’ Methode bewerkstelligt, wie folgendes Beispiel zeigt:
def myfunction():
print("Test my Function")
myfunction.__call__()
myfunction()
>>> Test my Function
>>> Test my Function
Ob ich also () oder __call__ verwende führt zum selben Ergebnis. (Denn auf oberster Ebene in der Python API, wird über __call__ die __get__ Funktion aufgerufen, welche das Objekt selbst konsumiert und dann aufruft.) Hier gibt es weitere Details, wer es noch genauer analysieren möchte.
Die Lösung zur Schwachstelle
Wir können also folgendes festhalten. Ein implizites Vertrauen in die korrekte Evaluierung eines Funktionsobjekts, gepaart mit dem Aufrufen der falschen Funktionalität (“Truth Value Testing” statt user.authorization.__call__), führte zu dieser Schwachstelle im Code.
Wie immer innerhalb der Domäne des Secure Codings gilt auch hier, traue niemals implizit einer Funktionalität, sondern validiere explizit. An dieser Stelle möchte ich die zweite Zeile des Zen of Python zitieren, welche auch mit “import this” aufgerufen werden kann.
Explicit is better than implicit.
Zen of Python
Wie könnten wir die Schwachstelle nun beheben? Es könnte nun argumentiert werden, dass alleine die korrekte Funktionsaufruf von user.authorization() die Schwachstelle beheben würde. Und ja, in diesem Fall wäre dies korrekt. Doch mit einer expliziten Definition innerhalb der if-Funktion wäre der Fehler entdeckt worden, denn die Autorisierung wäre für beide Benutzer fehlgeschlagen. Das implizite Vertrauen ist ebenso gefährlich wie das Vergessen des korrekten Funktionsaufrufs. Es gibt nun zwei saubere Lösungsansätze für das Problem.
Lösung 1:
Korrektes anrufen der Funktion mittels __call__ und explizites validieren der Autorisierung mittels “is True” in der if-Funktion:
####### Class definitions #########
class Users(object):
def __init__(self, username, password, isAdmin=False):
self.username = username
self.password = password
self.isAdmin = bool(isAdmin)
def authorization(self):
'''return the boolean value of isAdmin to check, if user is admin'''
return self.isAdmin
##################################
#### Authorization Function ######
def authorize(user):
if user.authorization() is True: ## <--- Explizites Überprüfen ob user Admin ist
myCompanySecret = "This is strictly confidential information, only for admins"
return print(myCompanySecret + " accessed by: " + user.username + ", is Admin: " + str(user.isAdmin))
else:
print("You don't have access for the super confidential Information, accessed by: " + user.username + ", is Admin: " + str(user.isAdmin))
##################################
#### Instantiate User objects ####
normalUser = Users("Peter", "myAmazingPwd", isAdmin=False)
adminUser = Users("Manuel", "thisIsSecure1234", isAdmin=True)
#################################
##### Check Authorization #######
authorize(normalUser)
authorize(adminUser)
#################################
Lösung 2:
- Benutzen des @property decorators für die Funktion authorization. Weitere Details unten.
- Explizites validieren der Autorisierung mittels “is True” in der if-Funktion.
####### Class definitions #########
class Users(object):
def __init__(self, username, password, isAdmin=False):
self.username = username
self.password = password
self.isAdmin = bool(isAdmin)
@property
def authorization(self):
'''return the boolean value of isAdmin to check, if user is admin'''
return self.isAdmin
##################################
#### Authorization Function ######
def authorize(user):
if user.authorization is True: ## <--- Exmplizites Überprüfen ob user Admin ist
myCompanySecret = "This is strictly confidential information, only for admins"
return print(myCompanySecret + " accessed by: " + user.username + ", is Admin: " + str(user.isAdmin))
else:
print("You don't have access for the super confidential Information, accessed by: " + user.username + ", is Admin: " + str(user.isAdmin))
##################################
#### Instantiate User objects ####
normalUser = Users("Peter", "myAmazingPwd", isAdmin=False)
adminUser = Users("Manuel", "thisIsSecure1234", isAdmin=True)
#################################
##### Check Authorization #######
authorize(normalUser)
authorize(adminUser)
#################################
Weitere Details zur Lösung 2:
Der @property decorator liefert syntactic sugar für den Compiler, damit Objekte wie Funktionen aufgerufen werden können. Der @property decorator liefert über ein descriptor object zusätzliche Methoden zurück, mit welchen gearbeitet werden kann. Lasst uns nun die Definition der property class betrachten:
class Property:
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"property '{self._name}' has no getter")
return self.fget(obj)
...
...
Wir erinnern uns nun an die oben erwähnte __call__ Funktion. Sie implementiert auf oberste Ebene die __get__ Funktion. Wenn wir nun die property class genau betrachten, sehen wir, dass __get__ das Objekt zurückgibt, wenn es aufgerufen wird. Vereinfacht gesagt bedeutet dies, dass mit dem @property decorator die Funktion aufgerufen wird ohne dass ich sie mittels “()” oder __call__ aufrufen muss. Wer Genaueres zur üblichen Verwendung des @property decorators erfahren möchte, kann ich diesen Blog-Beitrag empfehlen.
Abschliessendes
Sei beim Coding sehr explizit und vertraue nicht implizit auf irgendwelche Ergebnisse. Die Verwendung von truth value testing sollte vermieden werden. Dasselbe gilt übrigens für assertions zur Sicherheitsvalidierung (aber dies spare ich mir für einen zweiten Blog-Beitrag).
Ich hoffe, mein Beitrag hat geholfen. Ich freue mich auf Fragen oder Anmerkungen.
Comments