Defenzív programozás

A Wikipédiából, a szabad enciklopédiából
(Védekező programozás szócikkből átirányítva)

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[szerkeszté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[1000]; 
  
  // ...
  
  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[szerkeszté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[szerkesztés]

Túlságosan defenzív programozás[szerkeszté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[szerkeszté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[szerkesztés]

Túlságosan defenzív programozás[szerkeszté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[szerkeszté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[szerkesztés]

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

Forráskód intelligens újrafelhasználása[szerkesztés]

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[szerkesztés]

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,[1] a biztonságot, robosztussá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.[2]
  • 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.[3][4][5] 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[szerkeszté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[szerkesztés]

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[szerkesztés]

  • 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[szerkesztés]

  1. Paul Vixie and David Conrad on BINDv9 and Internet Security. impressive.net . (Hozzáférés: 2018. október 27.)
  2. Looking at the WMF issue, how did it get there?”, MSRC. [2006. március 24-i dátummal az eredetiből archiválva] (Hozzáférés ideje: 2021. május 1.) (amerikai angol nyelvű) 
  3. Litchfield, David: Bugtraq: Oracle, where are the patches???. seclists.org . (Hozzáférés: 2018. október 27.)
  4. Alexander, Kornbrust: Bugtraq: RE: Oracle, where are the patches???. seclists.org . (Hozzáférés: 2018. október 27.)
  5. Cerrudo, Cesar: Bugtraq: Re: [Full-disclosure RE: Oracle, where are the patches???]. seclists.org . (Hozzáférés: 2018. október 27.)

Források[szerkesztés]

További információk[szerkesztés]

Fordítás[szerkesztés]

Ez a szócikk részben vagy egészben a Defensive programming című angol Wikipédia-szócikk ezen változatának fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.