A defenzív programozás a defenzív tervezésnek egy olyan formája, amelynek az a szándéka, hogy biztosítsa a szoftver bizonyos részeinek folyamatos működését előreláthatatlan körülmények (például helytelen bevitelek) között. Gyakran használják, amikor szükség van magas szintű elérhetőségre és megbízhatóságra.

A defenzív programozás a szoftvert és a forráskódot hivatott javítani a következő szempontok alapján: Általános minőség – csökkenteni a szoftverhibákat és problémákat A forráskód érthetővé tétele – a forráskódnak olvashatónak és érthetőnek kell lennie, jóváhagyhatónak kódellenőrzéssel A szoftver viselkedését kiszámíthatóvá kell tenni váratlan bevitel és felhasználói cselekedetek esetén is

A túlzásba vitt defenzív programozás (mely olyan problémákat is próbál kezelni, amelyek soha nem merülhetnek fel) növeli a futási időt és a karbantartási költségeket; továbbá túl sok kivételt kezel, így potenciálisan észrevétlen, helytelen eredményeket adhat.

Biztonságos programozás

A biztonságos programozás a defenzív programozás egyik alfaja, amiben a számítógép biztonsága érintett. Itt a biztonság az elsődleges, nem a megbízhatóság vagy elérhetőség (lehet, hogy a szoftver bizonyos módon meghibásodhat). Mint mindenfajta defenzív programozásnak, ennek is szoftverhibák elkerülése az elsődleges célja; azonban a motiváció nem az, hogy csökkentse a meghibásodás valószínűségét normál működés esetén, hanem hogy csökkentse a támadási felületet. A programozónak tisztában kell lennie azzal, hogy a szoftvert nem a rendelkezésnek megfelelően használják, és megpróbálják rosszindulatúan kihasználni a szoftverhibákat.

int risky_programming(char *input) { char str;

// ...

strcpy(str, input); // Bevitel másolása.

// ... }

A függvény meghatározatlan viselkedést eredményez, ha a bevitel meghaladja az 1000 karaktert. Egyes kezdő programozók ezt nem tekintik problémának, mert feltételezik, hogy egyetlen felhasználó sem ír be ilyen hosszú bemenetet; a valóságban azonban lehetővé teszi a puffertúlcsordulás kihasználását. A megoldás erre az esetre:

int secure_programming(char *input) { char str[1000+1]; // Még egy karakter a null számára.

// ...

// Bevitel másolása a cél hosszának túllépése nélkül strncpy(str, input, sizeof(str));

// Ha strlen(input) >= sizeof(str), akkor az strncpy nem végződik null-lal. // Ekkor a puffer utolsó karakterét mindig null-ra állítjuk, // elvágva a bevitelt az általunk kezelhető maximális hosszúságnál. // Dönthetünk úgy is, hogy megszakítjuk a programot, ha a bevitel túl hosszú. str[sizeof(str) - 1] = '\0';

// ... }

Offenzív programozás

Az offenzív (támadó) programozás a defenzív programozásnak egy olyan kategóriája, amely arra összpontosít, hogy bizonyos hibákat nem kell defenzíven kezelni. Itt csak a kívülről származó hibákat (mint például a felhasználói bevitelt) kezelik, és megbíznak a szoftver, valamint a program védelmi vonalán belüli adatok helyességében.

Megbízás a belső adatok helyességében

Túlságosan defenzív programozás const char* trafficlight_colorname(enum traffic_light_color c) { switch (c) { case TRAFFICLIGHT_RED: return "red"; case TRAFFICLIGHT_YELLOW: return "yellow"; case TRAFFICLIGHT_GREEN: return "green";

return "black"; // Nem működő lámpaként kell kezelni.

// Figyelem: Az utolsó 'return' utasítás egy optimalizáló fordító elveti, ha a // 'traffic_light_color' összes lehetséges értéke szerepel a 'switch' utasításban }

Offenzív programozás const char* trafficlight_colorname(enum traffic_light_color c) { switch (c) { case TRAFFICLIGHT_RED: return "red"; case TRAFFICLIGHT_YELLOW: return "yellow"; case TRAFFICLIGHT_GREEN: return "green";

assert(0); // Ellenőrzés, hogy ez a szakasz elérhetetlen.

// Figyelem: Az 'assert' függvényt egy optimalizáló fordító elveti, ha a // 'traffic_light_color' összes lehetséges értéke szerepel a 'switch' utasításban }

Megbízás a szoftver-elemekben

Túlságosan defenzív programozás if (is_legacy_compatible(user_config)) { // Stratégia: Ne bízzunk abban, hogy az új kód ugyanúgy viselkedik old_code(user_config); } else { // Alternatíva: Ne bízzunk abban, hogy az új kód kezeli ugyanazokat az eseteket if (new_code(user_config) != OK) { old_code(user_config);

Offenzív programozás // Számítsunk arra, hogy az új kódnak nincsenek új hibái if (new_code(user_config) != OK) { // Tudassuk, hogy gond van, és lépjünk ki report_error("Ez nem jött össze"); exit(-1); }

Technikák Néhány defenzív programozási technika:

Forráskód intelligens újrafelhasználása Ha van egy tesztelt és működő kód, annak újrahasználata lecsökkenti az új hibák felbukkanásának esélyét. Ennek ellenére a kód-újrafelhasználás nem mindig jó gyakorlat, mivel felerősíti az eredeti kódra mért esetleges támadás következményeit. Ebben az esetben az újrafelhasználás komoly üzleti folyamathibákat okozhat.

Örökölt problémák Mielőtt újrafelhasználásra kerül a régi forráskód, könyvtárak, API-k, konfigurációk és így tovább, meg kell győződni arról, hogy van-e értelme azokhoz ragaszkodni, és nem fognak-e inkább probléma-öröklődéshez vezetni. Ha a régi megoldásoktól azt várják el, hogy a mai követelményeknek megfelelően működjenek (különösen akkor, amikor azokat nem fejlesztették vagy tesztelték az új igényeknek megfelelően), akkor valószínűleg örökölt problém1<k fognak megjelenni.

Sok szoftverterméknek problémája akadt a régi, örökölt forráskóddal:

Lehetséges, hogy a régi kódot nem a defenzív programozás alapján tervezték, így sokkal alacsonyabb minőségű lehet, mint egy újonnan írt forráskód. Lehetséges, hogy a régi kódot olyan körülményekhez írták és tesztelték, amelyek ma már nem alkalmazandóak. A régi minőségbiztosítási tesztek már nem érvényesek. Példák: A régi kódot ASCII bevitelre tervezték, de a mostani bevitel UTF-8. A régi kódot 32 bites architektúrákon kompilálták és tesztelték, de 64 bites architektúrákra fordítva aritmetikai problémák merülhetnek fel (előjelek, típuskényszerítés stb). A régi kódot offline gépekre írták, és a hálózati kapcsolat hozzáadásával sebezhetővé válik. A régi kód nem tartja szem előtt az új problémákat. Például egy 1990-ben írt forráskód valószínűleg sok kódinjekciós sebezhetőségre hajlamos, mivel a legtöbb ilyen problémát akkor még nem ismerték széles körben.

Ismert példák az örökölt problémákra:

BIND, amelynek problémáit Paul Vixie és David Conrad ismertette, a biztonságot, robusztusságot, skálázhatóságot és az új protokolokat megnevezve, mint a fő okokat, hogy újraírják a régi kódot. A Microsoft Windows „metafájl sebezhetősége” és a WMF formátumhoz kapcsolódó egyéb biztonsági rések. A Microsoft Security Response Center szerint mikor 1990 körül bevezették a WMF-támogatást, még nem a Microsoft biztonsági kezdeményezései alapján dolgoztak. Az Oracle is küzd az örökölt problémákkal, például a régi forráskóddal, amely nem kezelte a SQL-injekciót és a privilégium-eszkalációt, és ez számos biztonsági rést eredményezett, amelyek elhárítása időt vett igénybe, és egyesekre hiányos javításokat készítettek. Ez komoly kritikákat váltott ki olyan biztonsági szakértők részéről, mint David Litchfield, Alexander Kornbrust, Cesar Cerrudo. További kritika, hogy az alapértelmezett telepítések (nagyrészt a régi verziók örökségei) nincsenek összhangban az Oracle saját biztonsági ajánlásaival, például az Oracle Database Security Checklist-tel, amelyet nehéz módosítani, mivel sok alkalmazás megköveteli a örökölt (és kevésbé biztonságos) beállításokat a megfelelő működéshez.

Kanonizálás A rosszindulatú felhasználók valószínűleg újfajta adatábrázolásokat találnak ki helytelen bevitelhez. Például, ha egy program megpróbálja elutasítani az "/etc/passwd" fájlhoz való hozzáférést, akkor a cracker más formában adhatja meg a fájlnevet, például "/etc/./passwd". A kanonizációs könyvtárak alkalmazhatók a nemkanonikus bemenet által okozott hibák elkerülésére.

Alacsony tolerancia "potenciális" hibák ellen Tegyük fel, hogy a problémára hajlamos kódkonstrukciók (hasonlóan az ismert sebezhetőségekhez stb) programhibák és potenciális biztonsági hibák. Az alapvető ökölszabály: „Nem ismerem az exploitok minden típusát. Védekeznem kell azok ellen, amelyeket ismerem, utána pedig proaktívnak kell lennem!”

Egyéb technikák Az egyik legáltalánosabb probléma a konstans méretű struktúrák és függvények ellenőrizetlen használata dinamikus méretű adatokhoz (a puffertúlcsordulás problémája). Ez különösen általános a szöveges bevitelnél C-ben. Olyan C függvényeket, mint például a gets, soha nem szabad használni, mivel a bemeneti puffer maximális mérete nem szerepel paraméterként. Azonban a scanf biztonságosan használható, bár követelmény, hogy a programozó használat előtt ellenőrizze a bevitelt. A hálózatokon keresztül továbbított összes fontos adat titkosítása / hitelesítése. Ne próbáljunk meg saját titkosítási sémát megvalósítani, használjunk bevált sémákat. Minden adat fontosnak tekintendő, amíg ellenkezője be nem bizonyosodik. Minden adat hibásnak tekintendő, amíg ellenkezője be nem bizonyosodik. Minden kód károsnak tekintendő, amíg ellenkezője be nem bizonyosodik. Semmilyen kódnak a biztonságát nem lehet bebizonyítani, ami a felhasználóktól származik, vagyis „soha ne bízz az ügyfélben”. Ha egy adat helyességét kell ellenőrizni, akkor azt kell vizsgálni, hogy helyes-e, nem pedig azt, hogy helytelen-e. Szerződésalapú programozás A szerződésalapú programozás előfeltételeket, utófeltételeket és invariánsokat használ annak biztosítására, hogy a megadott adatok (és a program egészének állapota) tiszta. Ez lehetővé teszi a kód számára, hogy dokumentálja feltételezéseit, és biztonságos módon tegye ezt. Például a függvény vagy metódus argumentumai érvényességének ellenőrzése a függvény törzsének végrehajtása előtt. A függvény törzse után az állapot vagy más tárolt adatok, valamint a visszaadott érték ellenőrzése is javallott. Állítások (más néven asszertív programozás) A függvényeken belül kívánatos ellenőrizni (assert), hogy nem egy érvénytelen (null) adatot kívánunk referenciálni, és hogy a tömb hossza érvényes-e az elemek hivatkozása előtt, különösen az ideiglenes / helyi példányok esetében. Jó módszer az is, ha nem bízunk azokban a könyvtárakban, amelyeket nem magunk írtunk, és mindig ellenőrizzük az onnan kapott adatokat. Ezekhez gyakran ajánlott létrehozni egy kis "érvényesítő" és "ellenőrző" naplózó függvénykönyvtárat, hogy nyomon kövessük az utat és csökkentsük a debug szükségességét. A naplózási könyvtárak és a aspektusorientált programozás megjelenésével a védekező programozás körülményes aspektusai enyhültek. Kivételek visszaadása kódok helyett Általánosságban elmondható, hogy előnyösebb olyan érthető kivételüzeneteket dobni, amelyek dokumentálva vannak az API-ban egyszerű kódok visszaadása helyett.

Jegyzetek

Források

További információk CERT Secure Coding Standards

Fordítás

Kategória:Programozási paradigmák