Programare defensivă - Defensive programming

Programarea defensivă este o formă de proiectare defensivă menită să asigure funcționarea continuă a unui software în circumstanțe neprevăzute. Practicile de programare defensive sunt adesea folosite acolo unde este nevoie de disponibilitate ridicată , siguranță sau securitate .

Programarea defensivă este o abordare pentru îmbunătățirea software-ului și a codului sursă , în termeni de:

  • Calitate generală - reducerea numărului de erori și probleme software .
  • Facilitatea înțelegerii codului sursă - codul sursă ar trebui să fie lizibil și ușor de înțeles, astfel încât să fie aprobat într-un audit al codului .
  • Făcând software-ul să se comporte într-un mod previzibil, în ciuda intrărilor neașteptate sau a acțiunilor utilizatorului.

Cu toate acestea, programarea prea defensivă se poate proteja împotriva erorilor care nu vor fi întâlnite niciodată, atrăgând astfel costuri de execuție și de întreținere. Există, de asemenea, riscul ca capcanele de cod să prevină prea multe excepții , putând duce la rezultate incorecte neobservate.

Programare securizată

Programarea securizată este subsetul de programare defensivă care se ocupă cu securitatea computerului . Securitatea este preocuparea, nu neapărat siguranța sau disponibilitatea ( software - ul poate fi lăsat să eșueze în anumite moduri). Ca și în cazul tuturor tipurilor de programare defensivă, evitarea erorilor este un obiectiv principal; cu toate acestea, motivația nu este atât de mare pentru a reduce probabilitatea de eșec în funcționarea normală (ca și cum ar fi siguranța), ci pentru a reduce suprafața de atac - programatorul trebuie să presupună că software-ul ar putea fi utilizat în mod activ în mod activ pentru a dezvălui erori și că bug-urile ar putea fi exploatate cu răutate.

int risky_programming(char *input) {
  char str[1000]; 
  
  // ...
  
  strcpy(str, input);  // Copy input.
  
  // ...
}

Funcția va avea ca rezultat un comportament nedefinit atunci când intrarea este mai mare de 1000 de caractere. Este posibil ca unii programatori novici să nu simtă că aceasta este o problemă, presupunând că niciun utilizator nu va introduce o intrare atât de lungă. Această eroare specială demonstrează o vulnerabilitate care permite exploatarea depășirii bufferului . Iată o soluție la acest exemplu:

int secure_programming(char *input) {
  char str[1000+1];  // One more for the null character.

  // ...

  // Copy input without exceeding the length of the destination.
  strncpy(str, input, sizeof(str)); 

  // If strlen(input) >= sizeof(str) then strncpy won't null terminate. 
  // We counter this by always setting the last character in the buffer to NUL,
  // effectively cropping the string to the maximum length we can handle.
  // One can also decide to explicitly abort the program if strlen(input) is 
  // too long.
  str[sizeof(str) - 1] = '\0';

  // ...
}

Programare jignitoare

Programarea ofensivă este o categorie de programare defensivă, cu accentul adăugat că anumite erori nu ar trebui tratate defensiv . În această practică, trebuie tratate numai erorile din afara controlului programului (cum ar fi introducerea utilizatorului); software-ul în sine, precum și datele din linia de apărare a programului, trebuie să fie de încredere în această metodologie .

Încrederea în validitatea datelor interne

Programare prea defensivă
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"; // To be handled as a dead traffic light.
    // Warning: This last 'return' statement will be dropped by an optimizing
    // compiler if all possible values of 'traffic_light_color' are listed in
    // the previous 'switch' statement...
}
Programare jignitoare
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); // Assert that this section is unreachable.
    // Warning: This 'assert' function call will be dropped by an optimizing
    // compiler if all possible values of 'traffic_light_color' are listed in
    // the previous 'switch' statement...
}

Încrederea în componentele software

Programare prea defensivă
if (is_legacy_compatible(user_config)) {
    // Strategy: Don't trust that the new code behaves the same
    old_code(user_config);
} else {
    // Fallback: Don't trust that the new code handles the same cases
    if (new_code(user_config) != OK) {
        old_code(user_config);
    }
}
Programare jignitoare
// Expect that the new code has no new bugs
if (new_code(user_config) != OK) {
    // Loudly report and abruptly terminate program to get proper attention
    report_error("Something went very wrong");
    exit(-1);
}

Tehnici

Iată câteva tehnici de programare defensivă:

Reutilizarea inteligentă a codului sursă

Dacă codul existent este testat și se știe că funcționează, reutilizarea acestuia poate reduce șansa introducerii unor erori.

Cu toate acestea, reutilizarea codului nu este întotdeauna o bună practică, deoarece amplifică și daunele unui potențial atac asupra codului inițial. Reutilizarea în acest caz poate provoca erori grave ale procesului de afaceri .

Probleme de moștenire

Înainte de a refolosi vechiul cod sursă, bibliotecile, API-urile, configurațiile și așa mai departe, trebuie luat în considerare dacă vechea lucrare este valabilă pentru reutilizare sau dacă este probabil să fie predispusă la probleme vechi .

Problemele vechi sunt probleme inerente atunci când se așteaptă ca proiectele vechi să funcționeze cu cerințele actuale, mai ales atunci când proiectele vechi nu au fost dezvoltate sau testate având în vedere aceste cerințe.

Multe produse software au întâmpinat probleme cu vechiul cod sursă vechi; de exemplu:

  • Este posibil ca codul vechi să nu fi fost proiectat în cadrul unei inițiative de programare defensivă și, prin urmare, ar putea avea o calitate mult mai scăzută decât codul sursă nou proiectat.
  • Este posibil ca codul vechi să fi fost scris și testat în condiții care nu se mai aplică. Este posibil ca vechile teste de asigurare a calității să nu mai aibă valabilitate.
    • Exemplul 1 : codul vechi poate să fi fost proiectat pentru intrarea ASCII, dar acum intrarea este UTF-8.
    • Exemplul 2 : este posibil ca codul vechi să fi fost compilat și testat pe arhitecturi pe 32 de biți, dar atunci când este compilat pe arhitecturi pe 64 de biți, pot apărea noi probleme aritmetice (de exemplu, teste de semnătură nevalide, exprimări de tip nevalide etc.).
    • Exemplul 3 : este posibil ca codul vechi să fi fost vizat pentru mașini offline, dar devine vulnerabil odată cu adăugarea conectivității la rețea.
  • Codul vechi nu este scris având în vedere probleme noi. De exemplu, codul sursă scris în 1990 este probabil să fie predispus la multe vulnerabilități de injectare a codului , deoarece majoritatea acestor probleme nu erau pe larg înțelese în acel moment.

Exemple notabile ale problemei moștenite:

  • BIND 9 , prezentat de Paul Vixie și David Conrad ca „BINDv9 este o rescriere completă ”, „Securitatea a fost o considerație cheie în proiectare”, numind securitatea, robustețea, scalabilitatea și noile protocoale ca preocupări cheie pentru rescrierea vechiului cod vechi.
  • Microsoft Windows a suferit de " vulnerabilitatea " Windows Metafile și alte exploatări legate de formatul WMF. Centrul de răspuns la securitate Microsoft descrie caracteristicile WMF ca „În jurul anului 1990, a fost adăugat suportul WMF ... Acesta a fost un moment diferit în peisajul de securitate ... au fost complet de încredere” , nefiind dezvoltat în cadrul inițiativelor de securitate de la Microsoft.
  • Oracle combate problemele vechi, cum ar fi vechiul cod sursă scris fără a aborda preocupările legate de injecția SQL și escaladarea privilegiilor , rezultând multe vulnerabilități de securitate care au necesitat timp pentru a remedia și au generat, de asemenea, remedieri incomplete. Acest lucru a dat naștere unor critici grele din partea experților în securitate precum David Litchfield , Alexander Kornbrust , Cesar Cerrudo . O critică suplimentară este că instalările implicite (în mare parte o moștenire de la versiunile vechi) nu sunt aliniate cu propriile recomandări de securitate, cum ar fi Oracle Database Security Checklist , care este greu de modificat, deoarece multe aplicații necesită setările moștenite mai puțin sigure pentru a funcționa corect.

Canonicalizare

Este posibil ca utilizatorii rău intenționați să inventeze noi tipuri de reprezentări ale datelor incorecte. De exemplu, dacă un program încearcă să respingă accesarea fișierului „/ etc / passwd ”, un cracker poate trece o altă variantă a acestui nume de fișier, cum ar fi „/etc/./passwd”. Canonicalization bibliotecile pot fi folosite pentru a bugs Evitare din cauza non - canonice de intrare.

Toleranță scăzută împotriva bugurilor „potențiale”

Să presupunem că construcțiile de cod care par a fi predispuse la probleme (asemănătoare vulnerabilităților cunoscute etc.) sunt erori și potențiale defecte de securitate. Regula de bază a degetului mare este: „Nu sunt la curent cu toate tipurile de exploit de securitate trebuie să protejeze împotriva celor eu. Nu știu și atunci trebuie să fie pro - activ!“.

Alte tehnici

  • Una dintre cele mai frecvente probleme este utilizarea necontrolată a structurilor și funcțiilor de dimensiuni constante pentru date de dimensiune dinamică ( problema depășirii bufferului ). Acest lucru se întâmplă în special pentru șir de date în C . Funcțiile bibliotecii C getsnu ar trebui folosite niciodată, deoarece dimensiunea maximă a bufferului de intrare nu este transmisă ca argument. Funcțiile de bibliotecă C, cum scanfar fi, pot fi utilizate în condiții de siguranță, dar necesită programatorului să aibă grijă de selectarea șirurilor de format sigur, igienizând-o înainte de ao utiliza.
  • Criptați / autentificați toate datele importante transmise prin rețele. Nu încercați să implementați propria schemă de criptare, ci folosiți una dovedită.
  • Toate datele sunt importante până când nu se dovedește contrariul.
  • Toate datele sunt contaminate până când se dovedește contrariul.
  • Toate codurile sunt nesigure până când nu se dovedește contrariul.
    • Nu puteți dovedi securitatea vreunui cod în țara utilizatorului sau, mai canonic: „nu aveți niciodată încredere în client” .
  • Dacă datele trebuie verificate pentru corectitudine, verificați dacă sunt corecte, nu că sunt incorecte.
  • Proiectare prin contract
    • Proiectarea prin contract utilizează condiții prealabile , postcondiții și invarianți pentru a se asigura că datele furnizate (și starea programului în ansamblu) sunt igienizate. Aceasta permite codului să-și documenteze ipotezele și să le facă în siguranță. Aceasta poate implica verificarea argumentelor unei funcții sau metode pentru validitate înainte de a executa corpul funcției. După corpul unei funcții, este de asemenea înțelept să faci o verificare a stării sau a altor date reținute și a valorii returnate înainte de ieșiri (pauză / returnare / aruncare / cod de eroare).
  • Afirmații (numite și programare asertivă )
    • În cadrul funcțiilor, poate doriți să verificați dacă nu faceți referire la ceva care nu este valid (adică, nul) și că lungimile matricei sunt valide înainte de a face referire la elemente, în special la toate instanțierile temporare / locale. O euristică bună este să nu ai încredere în bibliotecile pe care nici tu nu le-ai scris. Deci, de fiecare dată când îi apelați, verificați ce primiți înapoi de la ei. De multe ori vă ajută să creați o mică bibliotecă de funcții de "afirmare" și "verificare" pentru a face acest lucru împreună cu un logger, astfel încât să vă puteți urmări calea și să reduceți nevoia de cicluri extinse de depanare . Odată cu apariția bibliotecilor de înregistrare și a programării orientate pe aspect , multe dintre aspectele plictisitoare ale programării defensive sunt atenuate.
  • Preferă excepții pentru a returna codurile
    • În general vorbind, este de preferat să trimiteți mesaje de excepție inteligibile care impun o parte din contractul dvs. API și să ghideze programatorul client în loc să returneze valorile pentru care un programator client este probabil să nu fie pregătit și, prin urmare, să minimalizeze reclamațiile acestora și să sporească robustețea și securitatea software-ului dvs. .

Vezi si

Referințe

  1. ^ "arhiva fogo: Paul Vixie și David Conrad pe BINDv9 și Internet Security de Gerald Oskoboiny <[email protected]>" . impresionant.net . Adus 27-10-2018 .
  2. ^ "Privind problema WMF, cum a ajuns acolo?" . MSRC . Arhivat din original la 24.03.2006 . Adus 27-10-2018 .
  3. ^ Litchfield, David. "Bugtraq: Oracle, unde sunt patch-urile ???" . seclists.org . Adus 27-10-2018 .
  4. ^ Alexander, Kornbrust. "Bugtraq: RE: Oracle, unde sunt patch-urile ???" . seclists.org . Adus 27-10-2018 .
  5. ^ Cerrudo, Cesar. "Bugtraq: Re: [Dezvăluire completă] RE: Oracle, unde sunt patch-urile ???" . seclists.org . Adus 27-10-2018 .

linkuri externe