Přejít k navigační liště

Zdroják » Různé » Kde se asi stala chyba: obsluha výjimek v Pythonu

Kde se asi stala chyba: obsluha výjimek v Pythonu

Články Různé

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 tomto článku se podíváme na ukázkový kód, který se zdá naprosto v pořádku, ale přesto obsahuje naprosto zásadní chybu.

Nálepky:

Článek původně vyšel na autorově blogu.

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 článku se podíváme na ukázkový kód, který se zdá naprosto v pořádku, ale přesto obsahuje naprosto zásadní chybu. Myšlenku na tento zápisek mi vnukl článek web.py – autentizace a autorizace.

Č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 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 potenciá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.

Komentáře

Odebírat
Upozornit na
guest
6 Komentářů
Nejstarší
Nejnovější Most Voted
potapec

Jak jsem jiz uvedl v komentari pod predchozim clankem, nasledujici zapis je chybuvzdorny:

AuthError = ( webmod.auth.UserNotFound, webmod.auth.WrongPassword )
try:
...
except AuthError, e:
...
pepa

Ta proměnná je tam vcelku zbytečná.

try:
    ...
except (webmod.auth.UserNotFound, webmod.auth.WrongPassword) as e:
    ...

A ještě je lepší dát výjímkám společnou base třídu a pak odchytávat jenom tu.

class AuthException(Exception):
    pass
class UserNotFound(AuthException):
    pass
class WrongPassword(AuthException):
    pass

try:
    ...
except AuthException as e:
    ...
IT expert

Promenna tam neni zbytecna. Hrozi, ze clovek zapmene na zavorky (zejmena v Py2.x) a jsme zpatky u puvodniho problemu.

A ještě je lepší dát výjímkám společnou base třídu a pak odchytávat jenom tu.
Jiste, pokud si vyjimky definujete svoje. Ale pokud pouzivate vyjimky z knihovny, tak tezko..

Ajtak

na zapomínání jsou nejlepší unit testy ;)

Martin Kubát @COEX

Zdravím z Plzně do Plzně ;-) a díky za upozornění na tuto zvláštnost.
Věřím, že tato finta může způsobit krásné, leč bezesné noci.
MK

Frugal computing: architektura pro dobu dražší infrastruktury

Vývojáři se naučili zrychlovat dotazy, přidávat cache, škálovat služby a hlídat účet za cloud. Frugal computing začíná o jednu otázku dřív: musí se výpočet, přesun dat, volání modelu nebo uložení vůbec stát? Rostoucí spotřeba datových center a nové evropské reportování ho posouvají do návrhu architektury, dřív než do závěrečné poznámky o udržitelnosti v prezentaci.

Odysseus: PewDiePie vydal open-source AI workspace, který běží na vašem vlastním hardwaru

AI
Komentáře: 0
Felix Kjellberg, youtuber se 110 miliony odběratelů, strávil rok učením se programovat a fine-tuningem vlastních AI modelů. Výsledkem je Odysseus – bezplatný, open-source workspace pro práci s umělou inteligencí, který neposílá žádná data do cloudu. Projekt má týden, přes 61 000 hvězdiček na GitHubu a znovu otevírá otázku, komu vlastně patří váš digitální kontext.

Když Git už nestačí: jak izolovat databázový stav pro pokusy AI agentů

Gitová větev vývojářům oddělí kód, ale databáze často zůstává společná. U AI agentů je to slabé místo: rychle spouštějí migrace, mění data a zkoušejí víc cest najednou. Databázová větev jim dá vlastní pracovní prostor, jenže tím práce nekončí. Ještě je potřeba řešit citlivá data, oprávnění, životnost větve i zbytek stavu aplikace.