3. fázis: Dokumentáció - koppa96/it-security-hw-2020 GitHub Wiki

1. Natív komponens

1.1. Architektúra

Az architektúra alapötlete az, hogy a program fő parse-olási funkcióit egy natív osztálykönyvtár tartalmazza, amit utána tetszőleges alkalmazásból fel lehet használni, mint statikus könyvtár (.lib fájl, C/C++ esetén), vagy mint dinamikus könyvtár (.dll / .so fájl, más nyelvekhez). A natív komponens egyetlen alkalmazásként is fordítható, ehhez további instrukciók a Readme oldalon találhatók.

1.1.1. Projekt struktúrája

A projekt 3 alprojektre osztható, ezek:

  • parser_core: a parser fő funkcionalitása, ez alkotja az osztálykönyvtárat.
  • parser_test: C++ teszt alkalmazás, amely felhasználja parser_core-t. Egyetlen alkalmazásként történő fordítás esetén, ez az alkalmazás fő projektje.
  • parser_test_cs: C# teszt alkalmazás, amely felhasználja parser_core-t. Példakódként szolgál arra, hogy hogyan lehet az osztálykönyvtárat más technológiákkal készült alkalmazásokban (pl. az elkészített webshop) felhasználni.

1.1.2. Külső interface

Az osztálykönyvtár kifelé egyetlen függvényt biztosít, ennek definíciója:

unsigned long long ParseAnimation(const char* in_buffer, unsigned long long in_len, char* out_buffer, unsigned long long out_len)

Bemeneti paraméterek:

  • in_buffer: bemeneti buffer, egy CAFF fájl nyers, bájtokban beolvasott tartalma.
  • in_len: bemeneti buffer hossza.
  • out_buffer: kimeneti buffer, ahová a hívó a kimenetet várja, tartalma a feldolgozott CAFF fájlhoz generált előnézet nyers bájtjai.
  • out_buffer_len: a kimeneti bufferben elférő bájtok maximális száma.

Kimenet: a kimeneti bufferbe írt adatok tényleges hossza (mindig kisebb vagy egyenlő kell, hogy legyen a kimeneti buffer maximális hosszával).

1.1.3. Főbb komponensek

  • LibApi: a külső interface definícióit tartalmazza.
  • Parser: feladata egy CAFF fájl tartalmának feldolgozása és egy Parse objektum előállítása, gyakorlatilag egy parse-olási folyamatként is felfogható. A folyamat eredményeként kiválasztódik az animáció képei közül az is, amely majd annak előnézeteként lesz felhasználva, ez tipikusan a leghosszabb ideig látható képet jelenti.
  • Parse: egy feldolgozott CAFF fájl adatait tartalmazó objektum.
  • ParseImage: egy Parse által tartalmazott objektum, amely egy az animáció egy képének (CIFF) adatait tartalmazza.
  • ImageBuilder: feladata, hogy egy ParseImage objektumból egy megjeleníthető képet generáljon, kimenete egy Preview objektum, ami a nyers képet és annak hosszát tárolja.
  • Defines: a program definícióit és konfigurációs beállításait tartalmazza.

1.2. Biztonság biztosítása

Natív komponensről lévén szó, a programban különös figyelmet fektettünk a memória kezelés helyességére és biztonságosságára, ennek során a direkt memória foglalások (new) helyett smart pointereket (shared_ptr) használunk, redukálva a felszabadítatlan memóriából adódó hibákat.

Ahol erre lehetőség van, figyelünk a túlindexelés elkerülésére, a maximális lehetséges hosszok (pl. foglalt memória méretéből adódóan) ellenőrzésével és tiszteletben tartásával.

Ezen felül különböző (konfigurálható) követelményeket írunk elő a feldolgozandó adatok hosszára (pl. adatok maximális mérete) és integritására vonatkozóan (pl. a fájlnak fejléccel kell kezdődnie, amiből csak egy darab fordulhat elő), az egyes problémás, illetve veszélyes bemenetek elkerülése céljából.

A fentiek mellett szigorúan ellenőrizzük, hogy a specifikációban megadott követelmények teljesülnek-e (pl. egy CIFF fájl tartalmának mérete a szélessége * magassága * 3 kell, hogy legyen), amennyiben nem, a bemenetet hibásnak minősítjük.

1.3. Tesztelés

A programot kézzel, saját bemenetekkel, illetve a fuzzer (AFL) segítségével teszteltük, utóbbinak egy példa kimenete alább látható:

A fuzzert többször, több számítógépen és környezetben is futtattuk, az eredményekről összességében elmondható, hogy 0 darab crash, illetve 0 darab hang jelentkezett, timeout-ok sajnos időnként, még a a program sebességre történő optimalizálása (a fenti kép még ezelőtt készült) után is előfordultak.

2. Szerveralkalmazás

2.1. Authentication

A felhasználók azonosítását az ASP.NET Core Identity-ből generált oldalak végzik. Bejelentkezéskor a kapott felhasználói adatok alapján ellenőrzi a jelszó helyességét, regisztrációkor a megadott adatokkal felhasználót készít, amelynek a jelszavát automatikusan hasheli, és salt-tal látja el. A felhasználói jelszó ellenőrzésére beépített metódussal rendelkezik, így közvetlenül nincs szükség hashelésre. A felhasználó adatai (azonosító, szerepkörök) egy sütibe kerülnek kiadásra bejelentkezés után, amelyet a hívó böngésző kap meg. Ez a cookie HttpOnly, JavaScriptből nem hozzáférhető, és a böngészési munkamenet végén lejár, így biztosítva a biztonságát.

Konfiguráltunk be továbbá kétfaktoros authentikációt, amelyet bejelentkezés után bármelyik felhasználó beállíthat. A jobb felső sarokban a "Hello xy!" feliratra kattintva a fiókbeállításainkba navigálhatunk ahol a "Kétfaktoros hitelesítés" menüpontra kattintva, az "Authentikátor app felvétele" feliratra kattintva megjelenik a képernyőn egy szöveges kód, és egy QR kód. Akár a szöveges kóddal, akár a QR kóddal beállítható az authentikátor. Jelenleg a Microsoft Authenticator és a Google Authenticator támogatott. A beállítás utáni következő bejelentkezéskor a sikeres jelszavas azonosítás után az oldal kérni fogja az Authentikátor appban megjelenő kódot. Lehetőség van a böngésző megjegyzésére, hogy legközelebb ne kérje a kódot, ha az aktuális böngészőből jelentkezünk be újra.

A kijelentkezéskor egyszerűen eltávolítjuk a felhasználó Cookie-ját. Az authentikációs rendszer egyetlen hátránya, hogy felhasználó kitiltása, valamint jogosultsági szintjének módosítása csak a következő bejelentkezéstől kerül érvényességre, mivel szerver oldalon a felhasználó jogosultságait a Cookie-ból olvassuk ki. Ennek kiküszöbölésére beállítottuk, hogy a Cookie-kat időnként hangolja össze az adatbázis tartalmával, ezt az intervallumot pedig 10 percre állítottuk be, így a szükséges módosítások 10 percen belül megjelennek a felhasználó Cookie-jában.

2.2. Authorization

A felhasználók autorizációját az ASP.NET Core implicit Authorization Middleware-je végzi. A bejelentkezéskor visszakapott cookie-k segítségével a rendszer minden híváskor eldönti, hogy az adott felhasználó rendelkezik-e a megfelelő jogosultságokkal az oldal megtekintéséhez. Ez lehet szerepkör-ellenőrzés, például a jóváhagyások esetében. Lehet azonban egyszerűen annak vizsgálata is, hogy be van-e jelentkezve a felhasználó, például a vásárlás oldalon.

Az alkalmazás az alábbi három jogosultsági szintet különbözteti meg:

  • ADMIN: Adminisztrátor felhasználó. Lehetősége van további felhasználókat adminisztrátorrá előléptetni, valamint felhasználókat kitiltani a rendszerből. A feltöltött, elbírálásra váró animációkat elfogadhatja vagy elutasíthatja. Bármelyik animációt letöltheti vásárlás nélkül is. Az egyes animációkhoz fűzött kommenteket törölheti, amennyiben tartalmát kifogásolhatónak tartja.
  • Bejelentkezett felhasználó: aki nincs explicit adminisztrátor szerepkörben de bejelentkezett a rendszerbe. A feltöltött animációk közül csak a már elfogadottakat tekintheti meg. Kommentelhet, de nem törölheti más kommentjeit. Csak az általa feltöltött, vagy az általa már megvásárolt animációkat töltheti le, illetve jogosult animációkat várásolni.
  • Bejelentkezés nélküli felhasználó: aki egyáltalán nem jelentkezett be. Számára csak az elfogadott animációk listázása érhető el. Bejelentkezés után kerül át a fenti két szerepkör egyikébe aszerint, hogy adminisztrátor-e vagy sem.

2.3. Accounting

A logolás Serilog használatával történik. A Serilog egy általános logging szolgáltatás, amely jól integrálódik ASP.NET Core-ral és több kimenetet is támogat párhuzamosan. A kritikus végpontok hívása logolásra kerül, ennek segítségével utólag felderíthető, ha egy felhasználó például gyanúsan sok kritikus végpontot hívott meg jogosulatlanul, vagy egyáltalán melyik végpontokat használta. Emellett a jelentősebb események és metaadataik (pl. a felhasználó képet vásárolt, letöltést kezdeményezett, stb.) az üzleti logikából is logolásra kerülnek.

A kritikus végpontok logolásakor mentésre kerül, hogy melyik felhasználó mikor melyik végponton milyen műveletet hajtott végre, és arra milyen státuszkódot kapott válaszul. Egyéb végpontoknál csak üzleti logikában történő explicit logolás történik, a felhasználó személyazonossága ekkor is rögzítésre kerül, amennyiben a műveletet bejelentkező felhasználó kezdeményezte.

Logolás egyszerre történik konzolra, valamint az ElasticSearch sinkjére is. A logfájlok naponta új indexbe kerülnek, így azok feldolgozhatósága, kereshetősége lényegesen javul. Ezek a logok később az ES által könnyen visszakereshetők, vetíthetők, transzformálhatók. Grafikus megjelenítésre a Kibana használata javasolt. Igyekeztünk a lehető legfontosabb eseményeket logolni, azonban előfordulhat, hogy így is nagy mennyiségű log keletkezik. Kibanával könnyen szűrhetők a logok, valamint a projekció is nagyon könnyű, így egyszerűen tudjuk vizsgálni a számunkra releváns bejegyzéseket.

2.4. Biztonsági megoldások

Az alkalmazás elkészítéséhez az ASP.NET Core Razor Pages nevű technológiáját alkalmaztuk. Ennek előnye, hogy automatikusan védve van az XSRF és CSRF támadások ellen (https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-5.0). Ez ugyan az automatikus tesztelést megnehezíti, de a fejlesztést jelentősen egyszerűsíti, mivel ezt se manuálisan kell kezelnünk. A Razor Pages továbbá védve van az XSS ellen is, ha mondjuk animációnévként egy rosszindulatú scriptet adunk meg, azt a HTML-be renderelve nem fog lefutni a script, csak szövegként megjelenik.

Az adatbázis kezeléshez az EntityFramework Core nevű osztálykönyvtárat használtuk, amely amellett, hogy kényelmes objektumor-relációs leképezést nyújt nekünk, és támogatja a Linq-t, a motorháztető alatt az ADO.NET-et használja, amely az SQL utasításokat SqlCommandok és Parameter-ek segítségével készíti el, így az adatbázis védve van a rosszindulatú SQL Injection támadásoktól.

Az alkalmazás a .NET Core CLR-ben fut, amely nagyban hasonlít a Java virtuális géphez. Nagy előnye, hogy a memóriakezelés nem a programozó felelőssége, a lefoglalt memória automatikusan kerül felszabadításra a Garbage Collector által. Az egyetlen memóriaszivárgási lehetőség, ha natív erőforrásokat használunk fel, és az azokra mutató kezelőket (handle) nem szabadítjuk fel. Erre a C# nyelv az IDisposable interfészt nyújtja megoldásként, és a using blokkot, amelyből kilépve (akár sikerrel, akár kivétel dobással) automatikusan felszabadítja a natív forrást.

2.5. Tesztelés

Az alkalmazást SonarQube használatával statikus analízisnek vetettük alá. A SonarQube az egyik legnépszerűbb statikus elemzési eszköz, többek között támogatja a C# nyelvet is.
A futás eredménye az alábbi képen látható: SonarQube result A képen látható, hogy biztonságosság szempontjából remekül vizsgázott az elkészült alkalmazás. A jelzett hibákat manuálisan felülvizsgáltuk, ezek java része jelentéktelen, a szemantikus HTML-lel, valamint nem használt using-okkal kapcsolatos, így ezeket egyelőre - idő hiányában - figyelmen kívül hagytuk.

A vizsgálat nem terjedt ki a generált kódra (EF Core migrációk), illetve a külső könyvtárakra. Ennek köszönhetően az eredmény az általunk megírt kód minőségét reflektálja, valós képet adva annak biztonságosságáról és megbízhatóságáról.

Az alkalmazáshoz készítettünk egy automatizált tesztprojektet is xUnit használatával. Az ebben található IntegrationTestBase osztály egy utility osztály, amely képes egy szolgáltatás-példányt felkonfigurálni és elindítani úgy, hogy adatbázisként egy in-memory adatbázist kap. Emellett elvégzi a kezdeti felhasználók feltöltését is.
Ezt követően az xUnit eszközeivel (Fact, Theory, Assert) integrációs teszteket készíthetünk, amelyekkel a rendszer egészét tesztelhetjük. Így például tesztelhető az authentikációs állapottól függő átirányítás vagy kiszolgálás-megtagadás. Természetesen ez a megoldás számos további tesztelési lehetőséget is elérhetővé tesz.

3. Telepítési útmutató

A szerveralkalmazás önmagában futtatható .NET Core alkalmazásként, azonban a futáshoz szükség lesz az Elasticsearchre, ugyanis abba logol a szerver. Az Elasticsearch host elérési útvonala dinamikusan konfigurálható a szerver appsettings.json fájlból, alapvetően a http://localhost:9200 URL-en keresi. A szervernek az Elasticsearch mellett még szüksége van a parser_core.dll-re, amely a lefordított natív komponens forráskódját tartalmazza. Ez jelenleg a megfelelő helyen van elhelyezve, a solution gyökérkönyvtárában, de alapvetően mindig az alkalmazás könyvtárának szülőkönyvtárában kell lennie.

A futtatási feltételek teljesítésének megkönnyítése érdekében összeállítottunk egy docker-compose.yml fájlt, amely tartalmazza az Elasticsearch-öt és a Kibanát megfelelően bekonfigurálva. A környezet elindításához navigáljunk a docker-compose-t tartalmazó könyvtárba és adjuk ki az alábbi utasításokat:

$ docker-compose up -d
$ cd CAFFShop.Api
$ dotnet run

A futáshoz szükség van egy MSSQL adatbázisra is. Az appsettings.json-ban ez is konfigurálható, alapértelmezetten egy MSSQLLocalDB-re van állítva, mely elérhető, ha a számítógépre van telepítve MS SQL Szerver.

A DLL előállítási módja miatt a rendszer csak Windows operációs rendszeren futtatható.