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:
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.
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';
// ...
}
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.
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
}
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
}
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);
}
}
// 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);
}
Néhány defenzív programozási technika:
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.
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:
Ismert példák az örökölt problémákra:
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.
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!”
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.