Címkék: #Biztonság (Security) #CSS #Érvényesítés (Validation) #HTML #Laravel #Támadás (Attacking) #Űrlap (Form) #Védekezés (Defence)
Validálás (érvényesítés) a weben
A
bejegyzés bevezetőjében azt mondtam, hogy a validálás egy többrétű
folyamat. Hogy tudunk a legegyszerűbben kapcsolatba lépni a
weboldalunkra érkező látogatókkal? Űrlapokon keresztül tudunk velük
kommunikálni. A kommunikáció legtöbbször egy regisztrációs űrlappal,
bejelentkezéssel, kapcsolatfelvételi űrlappal kezdődhet... aztán persze
attól függően, hogy mivel foglalkozik a weboldalunk, további felületeken
is kezdeményezhetünk kommunikációt, például egy webshop-os vásárlási
felületen keresztül, ahol el szeretnénk neki adni valamit és lehetőséget
akarunk biztosítani, hogy vásároljon tőlünk... vagy éppen csak egy
időpontfoglalási rendszert akarunk építeni, ekkor is egy űrlapon
keresztül tudunk kommunikálni a felhasználóinkkal. Látható, hogy
rengetegféle okkal lehet űrlapot építeni, már csak az a kérdés, hogy
hogyan tudunk érvényes információt szerezni a felhasználóinktól? Ebben
segít nekünk a validálás folyamata.
Onnantól kezdve, hogy a felhasználóinkkal kommunikálunk, már a legelején érdemes tisztázni, hogy ez már biztonság szempontjából kritikus folyamatot jelent, és mint tudjuk, tökéletes védelem nem létezik! De minél inkább igyekszünk megnehezíteni a támadó dolgát, annál inkább több időbe kerülne az oldalunk feltörése... feltűnhet, hogy támadót írtam, miközben eddig felhasználókról írtam. Előfordulhat persze, hogy az egyszeri felhasználó csak elrontana valamit az oldalon, aminek kapcsán a validálási folyamatunk segítene neki helyes irányba terelni az űrlap kitöltésének menetét. Én azért úgy szoktam kezelni, hogy minden, ami a felhasználótól bemenetként érkezik az űrlapunkra, arra tekintsünk úgy, mint ha ördögtől való lenne, és próbáljunk ellene védekezni minden lehetséges módon, és ellenőrizzük le többször a felhasználótól érkező adatokat, hogy biztosan ne okozhasson problémát a webalkalmazásunk működésében.
Azt írtam, hogy a validálás egy többrétű folyamat... adódhat a kérdés, hogy hol ellenőrizzünk? Kliens vagy szerver oldalon? A válasz leginkább az, hogy mindenhol IS, többször is, akár ugyanazt is. Azért, hogy megfelelő, érvényes adatokhoz juthassunk egy felhasználótól, ellenőrizzük a kapott értékeket kliens és szerver oldalon egyaránt. Ebben számos dolog segít minket: kliens oldalon a HTML5-ös szabvány (kiegészítve a CSS stílusokkal) valamint a Javascript kódok (és az összes validálást segítő Javascript osztálykönyvtár vagy keretrendszer) lehetnek a segítségünkre. Míg szerver oldalon majd a Laravel keretrendszer lesz főleg a segítségünkre.
Kissé laikusként feltehetnénk még azt a kérdést is, hogy ha validálunk, akkor megfelelő adatokhoz fogunk-e jutni a felhasználótól? A válasz nem mindig az igen. Ugyanis a validáció "csak" abban segít nekünk, hogy megfelelő formátumban kapjuk meg az adatokat, például, hogy ahol e-mail címet várunk el, az valóban egy e-mail cím szabályait követő cím lesz-e... de azt nem tudhatjuk, vagy csak nehézkesebben tudjuk ellenőrizni, hogy az valódi e-mail cím-e, létezik-e a valóságban, használja-e azt valaki. Vagy ha például a dátumra gondolunk, ami rengeteg formátumban elérhető, de általában az adatbázisunk csak egy adott formátumú dátum értéket képes befogadni. Ilyenkor azért, hogy "ne hasaljon el" az adatbekérési és feltöltési folyamat, már a kliens oldalon ellenőriznünk kell, hogy a felhasználó a nekünk (és az adatbázisunknak) megfelelő formátumban adta-e meg a dátumot. Viszont ha csak a formátumot ellenőrizzük a dátumnál, a korrektként elfogadható értéktartományt pedig nem, akkor könnyedén előfordulhat, hogy a felhasználó egy születési dátum mezőbe beír egy ezer évvel ezelőtti dátumot, ami nyilvánvalóan nem lesz megfelelő nekünk... Még számos példát tudnék hozni, de inkább majd megnézzük őket a gyakorlatban.
Mikor történik meg a bemeneti adatok validálása?
Mikor készítsük el és helyezzük el a validációs kódjainkat szoftverfejlesztőként?
Mondhatnám, hogy ez "nehéz"
kérdés, de igazából nem az, mert úgy kell építenünk az űrlapjainkat és
az azokhoz kapcsolódó kódjainkat, hogy már akkor rögtön megtörténjen a
validációs kódok elhelyezése, amikor épül az alkalmazás. Én, egy picit
lusta vagyok, úgyhogy biztos csak a saját véleményemet mondom el akkor,
amikor arra utalok, hogy én mindig arra szoktam törekedni, hogy először
egy működő folyamatot (adatbekérést és feldolgozást) rakjak össze és
aztán helyezem el a kódomban a validációs elemeket... ez biztosan egy
rossz szokás, úgyhogy az első verzió biztosan jobb, ne kövessétek az én
példámat jelen esetben.
Megjegyzés: a validálás szót nem csak az űrlap adatainak ellenőrzésekor szoktuk használni, hanem akkor is, amikor például a saját HTML-CSS kódunkat akarjuk érvényesíteni. Hiszen, ha ezeken nyelveken kódolunk, akkor be kell tartanunk szabványokat, szabályokat, amelyeket ha nem teszünk meg, akkor olyan problémákba futhatunk bele, hogy például a keresőoptimalizálásnál a weboldalunkat a keresőszolgáltatás (pl.: Google) hátrébb sorolja a találati listában, mert a weboldalunk kliens oldali kódja nem hibátlan. A weboldalunk adott címén elérhető kódját tudjuk validálni, vagy akár saját fájlok feltöltését is megtehetjük ezen az oldalon keresztül: https://validator.w3.org/ Ha a kódunk nem valid, érvényes, akkor pedig kiírja nekünk, hogy hol, melyik sorban található a hiba, figyelmeztetés és arra is ad javaslatot általában, hogy hogyan kellene azt kijavítani. Itt a wikipedia.hu weboldal kódjának validálási eredmény (részletét) láthatjuk:
Kliens oldali űrlap validáció HTML5 és CSS3 segítéségével (de egyelőre Javascript nélkül)
A kliens oldali validáció is két részre osztható:
A leggyakrabban használt beépített űrlap validációs HTML attribútumok (már a nevük is elég beszédes, de rövid magyarázatot azért írok hozzájuk):
Ezek tehát a HTML űrlapok bemeneti mezőinél validálásra alkalmazott attribútumok, amikkel rá tudjuk bírni a felhasználót bizonyos szabályok, formai követelmények betartására. A fejezet címébe beleírtam a CSS-t is, amire néhányan esetleg felkaphatták a fejüket, hogy azzal meg, hogyan lehetne validálni, hiszen azt leginkább a kinézet formázására használjuk. Pontosan ezt alkalmazhatjuk a validáláshoz is, tehát olyan kinézetet biztosítunk az űrlap mezőjének, amivel jelezzük a felhasználó számára, hogy valami nincs rendben, például nincs még kitöltve a mező, pedig kötelező lenne. Nézzünk meg néhány CSS pszeudo-osztályt, ami alkalmas lehet a probléma jelzésére:
Ezek tehát a legfontosabb HTML input mező attribútumok és CSS pszeudo osztályok, amelyeket a következő fejezetben ki is próbálunk a gyakorlatban.
Laravel alapú komplex példa tovább építése...
1. Függőségek frissítése
Mielőtt bármit is kezdenénk, hajtsuk végre a kódok, függőségek felfrissítését a komplex példa alkalmazásunkban eszerint a blogbejegyzés szerint, mert már régebben foglalkoztunk vele, úgyhogy biztosan sok minden frissült azóta. Én ehhez futtattam egy composer update parancsot, ami a lényegi szerver oldali függőségeket, csomagokat (vendor mappa) felfrissíti. Míg a kliens oldali csomagok szempontjából egy npm install parancsot is futtatok, hogy a node_modules mappa tartalmai is frissüljenek (esetleg itt, ha talál a rendszer kritikus vagy magas veszélyességű problémát, ütközést, akkor érdemes futtatni még egy npm audit fix parancsot is, amely megpróbálja ezeket a nehézségeket orvosolni).
2. Alkalmazás bővítése: utasok adatainak kiegészítése
Ezután válasszunk ki egy olyan űrlapot az alkalmazásunkban, amivel először el tudunk kezdeni játszadozni validálás szempontjából. Az én űrlapjaim egyelőre elég puritánok, egyszerűek, úgyhogy ha csak a kedves Olvasó nem egészítette ki saját megfontolásból korábban az űrlapjait, akkor hajtsunk végre közösen egy bővítést. Ehhez szükségünk van a következőkre:
Kezdjük is el a munkát szépen sorban!
2.1. Migrációs fájl és migrálás
php artisan make:migration add_properties_to_passengers_table
Ezzel létrejött az új migrációs fájl, ami alapból érzékeli, hogy mi a passengers táblához akarunk új mezőket hozzáadni. Az up() metódusba ez kerül:
Schema::table('passengers', function (Blueprint $table) { $table->unsignedInteger('age')->after('name'); $table->string('email'
, 100)->after('age'); $table->string('phone', 11)->after('email'); });
Az
after hívásokat csak azért használtam, hogy szépen sorba jöjjenek
egymás után a fontosabb oszlopok az adattáblában és még a created_at,
updated_at mezők elé bekerüljenek az itteni újak. Vegyük észre, hogy a
mezők értékkészleteinek szűkítése már itt megtörténik, amihez majd a
validációs szabályainknak alkalmazkodnia kell. Mindegyik mezőt kötelező
lesz megadni. Továbbá az életkor például nem lehet negatív egész szám,
az e-mail cím hossza maximum 100, míg a telefonszám hossza maximum 11
karakter hosszú lehet (itt majd betartjuk a korábban felvázolt
telefonszámra vonatkozó szabályt).
A down() metódusba ez kerül:
Schema::table('passengers', function (Blueprint $table) {
$table->dropColumn(['age', 'email', 'phone']);
});
A dropColumn metódushívás szerencsére kaphat tömböt is paraméteréül, így mindhárom új mező egy utasításban törölhető lesz majd ennek segítségével a passengers adattáblából. Ezután futtathatjuk a migrálást:
php artisan migrate
2.2. Passenger Model osztály
Következhet a Passenger Model osztály $fillable mezőjének kibővítése:
protected $fillable = ['name', 'flight_id', 'age', 'email', 'phone'];
2.3. és 2.4. Nézetek és kapcsolódó Controller metódusaik
Most jöhet a számunkra leginkább releváns rész, amikor az adatfeltöltő űrlapjainkat, nézeteinket bővítjük az új mezőkkel és friss validációs szabályokkal először a create-et (az utasneve mezőt bővítettük, a másik három páros újak):
<label for="utasneve">Utas neve:</label>
<input type="text" name="utasneve" id="utasneve" class="form-control mt-2 mb-4" placeholder="Utas neve" minlength="10" maxlength="50" required>
<label for="age">Életkor:</label>
<input type="number" name="age" id="age" class="form-control mt-2 mb-4" min="6" max="99" required>
<label for="email">E-mail cím:</label>
<input type="email" name="email" id="email" class="form-control mt-2 mb-4" maxlength="100" required>
<label for="phone">Telefonszám:</label>
<input type="text" name="phone" id="phone" class="form-control mt-2 mb-4" maxlength="11" placeholder="20/123-4567" pattern="[0-9]{2}[/][0-9]{3}[-][0-9]{4}" required>
A mintaillesztésnél használt minta magyarázata az elejétől: 0 és 9 közötti számokból kell 2 darab, utána 1 per jel, utána egy számhármas, majd egy kötőjel és jön újabb 4 szám, ha ennek a leírásnak megfelelünk, akkor kerül majd elfogadásra a kliens oldali validáció.
Most még nem fog működni akkor sem a kódunknál az adatfeltöltés, ha minden validációs szabálynak megfelelünk, mert még a PassengersController store() metódusából hiányoznak a megfelelő értékek átadásai. Csináljuk ezt meg:
public function store(Request $request)
{
Passenger::create([
'name' => request('utasneve'),
'flight_id' => request('repulojarata'),
'age' => request('age'),
'email' => request('email'),
'phone' => request('phone'),
]);
return redirect('passengers');
}
Persze rövidíthetnénk is ezt a kódsorozatot, ha az "utasneve" és "repulojarata" mezőket átírnánk a nézet fájlunkban "name"-re és "flight_id"-re, mert utána itt a store() metódusban már csak simán a Passenger::create() metóduson belül a request()->all() -t tudnánk használni. Viszont enélkül is most már tökéletesen menni fog a mentés a passengers adattáblába.
Rátérhetünk az edit nézet fájlunkra, ahova szintén beillesztem az új mezőket és akár egy kis trükköt alkalmazhatnék, ha nem lenne elég széles a monitorom: ALT + SHIFT + f billentyűkombinációval tudjuk formázni a kódunkat, mert most már az input HTML tag-ünk túl sok információt tartalmaz (én nem tettem még meg, de ti kipróbálhatjátok). Így néz ki az edit nézet (űrlap) frissített / bővített része:
A create-hez képest csak a value attribútumokkal és értékeik beállításaival bővültek az input mezők, a többi mind ugyanaz.
A PassengersController update() metódusa szerencsére már fel van készülve a módosítások fogadására, így azzal nincs teendőnk.
2.5. Új stílusszabályok
Még egy dolog van hátra, mielőtt rátérnénk a tesztelésre. A resources / sass / mystyle.scss fájlunkban a CSS pszeudo-osztályokat módosítsuk azért, hogy jobban láthatók legyenek a validációs hibák (jelzések) az oldalunkon.
input:invalid {
box-shadow: 0 0 5px 1px red;
}
input:out-of-range {
background-color: rgba(255, 0, 0, 0.25);
border: 2px solid red;
}
form:invalid input[type="submit"] {
opacity: 0.3;
pointer-events: none;
}
Az :invalid pszeudo-osztály stílusa miatt a megnyíló űrlapunk minden nem érvényes bemeneti mezője kapni fog egy piros árnyékolást, miután megadtuk nekik az érvényes értéket, akkor fog eltűnni ez az árnyék. Az :out-of-range következtében az életkor mezőnk háttere és kerete lesz piros, amennyiben 6-nál kisebb vagy 99-nél nagyobb értéket írunk bele (és utána lépünk ki belőle, tehát elvesszük róla a fókuszt). Az utolsó selector a Mentés gombra vonatkozik, annyit csinál, hogy amíg nem valid, érvényes az űrlapunk, addig a Mentés gomb halvány lesz és nem is lesz kattintható. Mindezek eléréséhez azonban futtatnunk kell a terminal-ban a következő utasítást:
npm run dev
Ha nem emlékeznél, hogy mit csinál ez, akkor pillants rá erre a bejegyzésre. Ennek hatására készül el a public / css mappába a mystyle.css lefordított fájlunk. Ha a létrehozó (create) űrlap újratöltésekor nem kapnának piros árnyékolásokat a mezők, akkor próbálkozzunk a SHIFT + frissítés gomb megnyomásával a böngészőnkben Firefox-ban vagy CTRL-lal Chrome-ban.
Példák a tesztelés során kapott figyelmeztetésekre
Először még a CSS módosítások nélkül teszteltem. A kliens oldali validációkor (Mentés gomb megnyomásakor) felugró hibajelzések nyelve alapértelmezetten a böngésző nyelvének beállításától függ, nálam például angol:
A mező kitöltése kötelező.
A mezőbe csak 9 karaktert írtam a minimálisan elvárt 10 helyett.
Miután pedig beállítottam a CSS pszeudo-osztályokat is, akkor így változott a problémás input mezők kinézete (kivágtam az oldal releváns részét, ezért nem látszódik a jobb oldali árnyékolás):
Az :out-of-range pszeudo-osztály eredménye:
Ezután, ha mindenhova érvényes értéket adunk meg, akkor a Mentés gomb aktívvá válik és menthető is az új utasunk az adattáblájába.
Kliens oldali validáció támadása és feltörése
Miért is kell mindenhol IS validálnunk a felhasználóktól származó adatokat?
Azért, mert a kliens oldali validáció nagyon egyszerűen "feltörhető" és az alkalmazásunkat megtámadhatják, illetve nem ellenőrzött működéseket idézhetnek elő a rendszerünkben, ha csak kliens oldalon validálunk. Nézzünk meg erre egy példát.
Nyissuk meg a felhasználó létrehozó űrlapját, majd jobb egérgomb valamelyik kötelezően kitöltendő input mezőn és Vizsgálat menüpont kiválasztása, aztán alul / oldalt (attól függően, hogy hol jelenik meg a weboldal kódja, DOM fája), ott az input mezők tartalmát tudjuk szerkeszteni. Töröljük ki a név, életkor, e-mail cím, telefonszám mezőknél a required="" attribútumot. Ugye ez volt a felelős azért, hogy a mező kitöltése kötelező legyen. Miután egyesével kitöröljük ezeket, akkor aztán már meg is szűnik a piros árnyék a mezőknél, mintha érvényesek lennének... hiszen azok is, mert már elvileg (!) nem elvárás tőlük, hogy kötelezően ki legyenek töltve. Az utolsó required törlésénél a Mentés gombunk is kattinthatóvá válik.
Úgyhogy ha üresen hagyjuk a mezőinket és kattintunk a Mentésre, akkor kapni fogunk egy szép nagy hibaüzenetet (exception-t): mivel bár a kliens oldali validáción "átment" a rendszer a hackelésnek köszönhetően, de adatbázis szinten ezeket a mezőket kötelező lenne kitölteni. Erről szól a következő hibaüzenet (bár csak a name mezőt említi, de ha az nem lenne üres, akkor már reklamálna az age, majd az email, majd a phone miatt is):
Mennyivel szebb lenne, ha szerver oldalon is ellenőriznénk az adatok "kötelezően kitöltöttségét", tényleges meglétét. Ha pedig nem teljesülne ez az elvárás, akkor érdemileg visszajeleznénk a felhasználónak, hogy valamelyik mezőt elfelejtette kitölteni, pótolja a hiányosságot... ezt fogjuk majd megnézni a szerver oldali validációnál, hogy hogyan kell végrehajtani.
Szerencsére a Passenger Model osztályban lévő $fillable változó segítségével védjük az id mezőnket, de gondoljunk csak bele, mi lenne akkor, ha csak egy $guarded = []; utasítással "védenénk" (tehát kvázi nem védenénk) a táblánk mezőit a Model osztályban. Aztán valaki csinálna az iménti módon a DOM fa szerkesztése során egy input mezőt, amiben elküldi az id értékét (value), önkényesen megválasztva azt. Ebből több gond is lehet, kettőt kiemelek:
Ellenőrizzünk tehát mindig, minden oldalon (kliens és szerver oldalon egyaránt) azért, hogy magasabb fokú biztonságot érjünk el a kódunkban, és ne lehessen könnyen megtámadni, feltörni.
A blogbejegyzéshez tartozó változások ebben a Github commit-ben érhetők el a komplex alkalmazásunkban.
A következő Laravel-es blogbejegyzésben maradok majd a kliens oldali validációnál, de már a Javascript kódokkal kiegészített validálást vesszük át.