Miért kell az óvatosság az eval() függvény használatakor?

Tegyük fel, hogy egy olyan alkalmazást szeretnénk írni, amelyben a felhasználó által grafikus felületen vagy más, szövegbevitelre alkalmas eszközön megadott egyváltozós matematikai kifejezést (képletet) kell kiértékelni a független változó adott értékénél, majd pedig az eredményt feldolgozni. Ahhoz, hogy fel tudja a programunk ismerni a független változót, ezért annak jelölését előre rögzítjük: legyen a matematikában szokásos x betű. A képlet tartalmazhat olyan matematikai függvényeket, amelyek vagy beépített függvények (pl. abs(), pow(), round() ), vagy a szabványos könyvtár math moduljában szerepelnek (pl. sqrt(), sin(), cos(), log(), exp(), stb.).

A kifejezés kiértékelésére kézenfekvő, hogy az eval() beépített függvényt használjuk. De mivel lehet téves adatbevitel, ami nem értelmezhető a kiértékeléskor, és előfordulhat nullával való osztás is, ezért célszerű a kiértékelés egy egyéni függvényben elvégezni, ahol a kivételeket is kezelni tudjuk. Erre mutatunk példát alább. Itt azt is megfigyelhetjük, hogy hiba esetén dobandó kivételekhez definiáltunk egy egyéni kivételosztályt, amellyel egységesen tudjuk a kivételkezelést végrehajtani, és amelynek nevéből rögtön következtetni lehet, hogy a programunk mely részéről származik a hiba.

A tesztelést a következő programsorokkal végezzük, ahol különböző karakterláncokat adunk át az eval_str() függvénynek, és nézzük, hogy milyen eredményt kapunk.

Megállapíthatjuk, hogy egészen az utolsó bemeneti adatig a tervezettnek megfelelően működik a függvény. Az utolsó sor egy rosszindulatú támadást szimulál, ahol e karaktersor kiértékelését is kikényszerítjük az and logikai operátor alkalmazásával. Ennek bal oldali operandusa azonban nem egy matematikai kifejezés, hanem egy függvényhívás. Ez sajnos le is futott, ami egy adott fájl törlését eredményezte volna, ha a fájl létezik.

Az utolsó sorból ránézésre is nyilvánvaló, hogy egy nem kívantos karaktersorozat lett megadva. De az előtte levő sor sem veszélytelen. A beépített getattr() és setattr() is használható lenne rosszindulatú kód futtatására. Ezért az sem lenne jó, ha ezek kiértékelésre kerülnének. De valójában a beépített függvények többsége sem illik egy egyvátozós matematikai képletbe, legfeljebb csak a kifejezetten matematikai függvények, mint a fentebb említett abs(), pow(), round(), de az int() is ennek tekinthető.

Az utolsó sor káros kódjának kiértékelését két tényező tette lehetővé. Az egyik az, hogy megengedtük a képletekbe szintén nem illő dunder attribútumneveket (azon neveket, amelyek két aláhúzással kezdődnek és végződnek). Másrészt megengedtük az attribútumelérést, vagyis a pont (‘.’) operátor használatát.

Hogy a potenciális károkozást elkerüljük, el kell érni egyrészt azt, hogy a pont operátor ne működjön, másrészt, hogy a dunder attribútumneveket az eval() függvény ne tudja értelmezni.

A pont jel egyszerű kiszűrése a bemeneti karaktersorozatból nem járható út, mert a pont egyben tizedes elválasztó jel is. Matematikai kifejezésekben gyakran szerepelnek. Ezért csak azt tudjuk megmondani, ha biztosan kizárható, hogy a pont jel Pythonban szabályos tizedestört része. Például, ha előtte zárójel van, akkor az biztos, hogy nem tizedestört. Azt, hogy a pont milyen más megelőző karakterrel együtt lehet egy tizedestört része, alaposan át kell gondolni. Ha ez megvan, akkor minden olyan esetben, ahol a pontot nem egy megengedett karakter előzi meg, vissza kell utasítani a bemenő sorozatot. A kiszűrést végző függvény egy lehetséges megvalósítása látható alább. A részletes kommentek segítik a működés megértését.

A második szűrési feladat, hogy a dunder attribútumokat beazonosítsuk a karakterláncban, ha vannak. Ennek megvalósítását mutatja a következő függvénydefiníció:

Van tehát két függvényünk, amelyekkel még a kiértékelés előtt kiszűrhetjük azokat a felhasználó által bevitt karaktersorozatokat, amelyek nemkívánatos vagy károkozásra alkalmas utasításokként értelmezhetők. De ez még nem elég. Az eval() függvénynek meg kell adni, hogy a beépített függvények közül melyeket szabad csak értelmezni. Továbbá tudatni kell vele, hogy mely karaktert tekintse független változónak, és melyek a math modul használható függvénynevei.

Az eval() függvény úgy képes ezekről tudomást szerezni, ha az opcionális globals, és locals paramétereinek átadjuk a megfelelő névtereket reprezentáló szótárakat, amelyekben már csak az általunk engedélyezett nevek szerepelnek.

Az ‘x’ karakterhez mint névhez az eval_str() függvény x paraméterének értéke fog társulni. Ez lokális változó, tehát az eval() locals argumentuma lesz. Ha a math modult a függvényen belül importáljuk, akkor a matematikai függvénynevek is lokális nevek lesznek. Tehát ha ezeket egy szótárba gyűjtjük, akkor az x nevet tartalmazó szótárral egyesítve adhatjuk át az eval() locals paraméterének. Azért célszerű így tenni, mert ekkor az eval() globális neveinek meghatározása leegyszerűsítődik: a globals paraméternek csak egy olyan szótárt kell átadi, amelyben egyetlen kulcs van, a  ‘__builtins__’, amelyhez csak a beépített matematikai függvények név-objektum párjai társulnak.

Az eval_str() függvény ezen elvek alapján módosított változata alább látható. A részletes kommentek itt is segítik a megértést.

/Aki a reguláris kifejezésekben (regex) járatos, az a szűrőfeltételeket a bemutatott függvények helyett azzal is meghatározhatja, és megvalósíthatja a szabványos könyvtár re moduljának használatával./

Ha a következő teszteredményeket megnézzük, akkor megállapíthatjuk, hogy a függvénynek nem kívánatos művelet kiváltására alkalmas értelmezhető kód nem adható.

Fontos tudni, hogy a rosszindulatú kódok eval() függvény által történő értelmezésének kivédése a bemutatott módon, azaz előszűrések és névtérszűkítéssel ebben a speciális, nagyon körülhatárolt esetben (csak egyváltozós matematikai kifejezés az elfogadható) elérhető volt. Általánosan azonban ilyen módon nem lehet garantált védelmet biztosítani, hanem a legbiztosabb módszer az adott feladathoz illeszkedő egyéni értelmező készítése és alkalmazása. Ezért kell az eval() függvényt – és az exec() függvényt is – kellő óvatossággal, átgondoltan használni.

Ebben a bejegyzésben az eval() mellett szó volt általában a beépített függvényekról és a speciális metódus- és adatattribútumokról, amelyeket a szakzsargon dunder attribútumoknak is nevez a double underscore szavak összevonásából képezve. A kivételek és kivételkezelés is jelentős szerepet kapott, és láthattunk egyéni kivételosztály definiálására és használatára példát. Ezen kívül a névterek ismerete is szükséges volt a megfelelő implementációhoz. Mindezekről a Python tudásépítés lépésről lépésre című e-könyv „Beépített függvények”, „Kivételes bánásmód – kivételek és kezelésük”, „Egyéni kivételtípusok megvalósítása”, „Hatókörök és névterek”, valamint a „Speciális metódus- és adatattribútumok” fejezetekben lehet részletesen olvasni.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.