Python, stejně jako většina moderních programovacích jazyků, obsluhuje chyby vzniklé za běhu pomocí výjimek. Pro každého programátora v Pythonu je blok try ... except
základem všeho bytí. Výjimky se používají nejen pro obsluhu běhových chyb a chyb vzniklých v operačním systému, ale velice úzce souvisí i s dynamickým typováním samotného Pythonu (viz příklad zde). V tomto zápisku se podíváme na ukázkový kód, který se zdá naprosto vpořádku, ale přesto obsahuje naprosto zásadní chybu. Myšlenku na tento zápisek mi vnukl článek na serveru Zdroják.cz.
Článek samotný se věnuje autentizaci a autorizaci uživatelů ve frameworku web.py. Autor Petr Horáček v něm popisuje svůj vlastní modul, který tuto problematiku řeší. Nicméně ukázkový kód zveřejněný ve článku obsahuje chybu v obsluze výjimek vzniklých při přihlašování uživatele (viz můj komentář). Tato chyba byla natolik inspirativní, že jsem se rozhodl ji dále rozebrat v tomto blogu. Tato chyba se týká Pythonu 2.x, v Pyhonu 3 je zápis, který na chybu vede, považován za syntaktickou chybu.
Chyba není na první pohled vůbec zřejmá, kód funguje, vše je syntakticky správně, ale přece jen něco není v pořádku. Pro ilustraci v tomto zápisku nejprve modul, který definuje uživatelské výjimky jako třídy (soubor nazvěme jej mod_exc.py
):
class UserNotFound(Exception): pass class WrongPassword(Exception): pass
Nyní již samotný kód, za funkcí POST()
si představte obsluhu HTTP POST požadavku, kterému jsou předány parametry user
a passwd
obsahující jméno a heslo uživatele k přihlášení, tyto údaje jsou ověřovány ve funkci login()
oproti slovníku PWD_DICT
. Funkce login() proběhne pokud je vše v pořádku, jinak vyhodí výjimku UserNotFound
nebo WrongPassword
:
import mod_exc import sys PWD_DICT = {'honzas': 'hPsWd'} def login(user, passwd): try: if PWD_DICT[user] != passwd: print 'wrong password' raise mod_exc.WrongPassword except KeyError: print 'user not found' raise mod_exc.UserNotFound def POST(**args): usr = args.get('user') passwd = args.get('passwd') try: login(usr, passwd) print 'access granted' except mod_exc.UserNotFound, mod_exc.WrongPassword: print 'access denied' print POST(user='honzas', passwd='hPsWd') POST(user='test', passwd='hPsWd') POST(user='honzas', passwd='foobar') POST(user='test', passwd='hPsWd') POST(user='honzas', passwd='foobar')
Výstup je dle očekávání:
access granted user not found access denied wrong password access denied user not found access denied wrong password access denied
Nicméně kód obsahuje jednu naprosto zásadní (a opovažuji se říci v tomto kontextu i potencionálně bezpečnostní) chybu. Prvním indikátorem je již to, že pokud se pokusíme upravit posloupnost volání funkce POST()
následujícím způsobem, získáme traceback namísto očekávaného výstupu.
Upravené volání:
POST(user='honzas', passwd='hPsWd') POST(user='honzas', passwd='foobar') POST(user='test', passwd='hPsWd')
A výstup:
access granted wrong password Traceback (most recent call last): File "exceptions.py", line 28, in POST(user='honzas', passwd='foobar') File "exceptions.py", line 20, in POST login(usr, passwd) File "exceptions.py", line 10, in login raise mod_exc.WrongPassword mod_exc.WrongPassword
Stopujeme brouka…
Přidáme si ladící výpisy do funkce POST()
, abychom měli ponětí o tom, co se děje:
def POST(**args): usr = args.get('user') passwd = args.get('passwd') try: login(usr, passwd) print 'access granted' except mod_exc.UserNotFound, mod_exc.WrongPassword: print 'access denied' type, value, traceback = sys.exc_info() if type == mod_exc.UserNotFound: print '... user not found' else: print '... wrong password' print POST(user='honzas', passwd='hPsWd') POST(user='test', passwd='hPsWd') POST(user='honzas', passwd='foobar') POST(user='test', passwd='hPsWd') POST(user='honzas', passwd='foobar')
Výstup pak dokáže docela překvapit:
access granted user not found access denied ... user not found wrong password access denied ... user not found user not found access denied ... user not found wrong password access denied ... user not found
Ve všech případech se ukazuje, že je odchycena výjimka UserNotFound
a to i pokud je uživatelské jméno správně a je předáno chybné heslo!
Kde se asi stala chyba
Obecně lze říci, že chyba vznikla již při živelném návrhu Pythonu 2.x. Při snaze o očištění světa, na jejímž konci je Python 3.x, bylo chybě efektivně zabráněno již při syntaktické kontrole kódu. Chybu totiž způsobují chybějící závorky ve větvi except (v kódu se očekává odchycení výjimek dvou tříd):
except mod_exc.UserNotFound, mod_exc.WrongPassword:
Takovýto zápis je však ekvivalentní odchycení výjimky třídy UserNotFound
pod jménem mod_exc.WrongPassword
:
except mod_exc.UserNotFound as mod_exc.WrongPassword:
Namísto odchycení výjimek buď jedné, nebo druhé třídy, se vždy odchytí pouze výjimky třídy UserNotFound
a uloží se do modulu mod_exc
pod jméno WrongPassword
! A následně, pokud se vyvolá výjimka mod_exc.WrongPassword
, tak ve skutečnosti dojde opět k vyhození UserNotFound
.
Oprava
Oprava kódu je v tomto případě velice jednoduché, z programu s opravenou funkcí POST() již získáme správné výstupy:
access granted user not found access denied ... user not found wrong password access denied ... wrong password user not found access denied ... user not found wrong password access denied ... wrong password
Závěr
Přestože výše popsaný problém ovlivňuje pouze Python 2.x, je chyba, vzhledem k jeho rozšíření a stále aktivní podpoře, velice zásadního typu a dala by se přirovnat k přepsání kódu programu vhodně navrženou posloupností vstupů, což obecně může vést k bezpečnostím chybám.
Povšimněte si, že chyba je způsobena především vyvezením definice výjimek do samostatného modulu, importem tohoto modulu a odkazování výjimek pomocí tečkové notace. Jinými slovy odkazuji se na jména uvnitř jiného modulu namísto na jména získaná z globálního namespace (ta bez jejich definice jako global
nelze měnit). To umožní trvalé přepsání jednoho jména jiným objektem a modifikaci chování následně vykonaného kódu.
Nakonec kopnutí do Python 2.x: je jedině dobře, že zmíněný zápis již není podporován v Python 3.x; jedná se o historickou zátěž způsobenou chaotickým vývojem Pythonu v časech pradávných.