Laravel biztonsági megoldások

Laravel biztonsági megoldások

A következő biztonsági mechanizmusokat tekintjük át:

  • N+1 probléma
  • Részben/teljesen hidratált modellek
  • Mass Assignment védelem
  • Strict mód

N+1 probléma és megelőzése

Sok ORM, köztük az Eloquent is kínál olyan „funkciót”, amely lehetővé teszi a modell kapcsolatának lazy betöltését. A lazy betöltés kényelmes, mert nem kell előre gondolkodnia azon, hogy melyik kapcsolatokat válassza ki az adatbázisból, de gyakran az „N+1 probléma” néven ismert teljesítményrémálomhoz vezet.

Az N+1 probléma az egyik leggyakoribb probléma, amellyel az emberek szembesülnek az ORM használata során, és gyakran ez az oka annak, hogy teljesen elkerülik az ORM-eket. Ez egy kis túlkorrekció, mivel egyszerűen letilthatjuk a lazy betöltést!

Képzeld el a blogbejegyzések naiv felsorolását. Megjelenítjük a blog címét és a szerző nevét.

$posts = Post::all();

foreach($posts as $post) {
    // `author` is lazy loaded.
    echo $post->title . ' - ' . $post->author->name;
}

Ez egy példa az N+1 problémára! Az első sor az összes blogbejegyzést kiválasztja. Ezután minden egyes bejegyzésnél egy újabb lekérdezést futtatunk, hogy megkapjuk a bejegyzés szerzőjét.

SELECT * FROM posts;
SELECT * FROM users WHERE user_id = 1;
SELECT * FROM users WHERE user_id = 2;
SELECT * FROM users WHERE user_id = 3;
SELECT * FROM users WHERE user_id = 4;
SELECT * FROM users WHERE user_id = 5;

Az „N+1” jelölés abból a tényből származik, hogy az első lekérdezés által visszaadott n-sok rekord mindegyikéhez egy további lekérdezés fut. Egy kezdeti lekérdezés plusz n-sok további. N+1.

Annak ellenére, hogy minden egyes lekérdezés valószínűleg meglehetősen gyors, összességében hatalmas teljesítménybüntetést láthat. És mivel minden egyes lekérdezés gyors, ez nem jelenik meg a lassú lekérdezési naplóban!

A Laravel segítségével a Model osztályban a preventLazyLoading metódust használhatja a lusta betöltés teljes letiltásához. Probléma megoldódott! Valójában ez ilyen egyszerű.

A módszert hozzáadhatja az AppServiceProviderhez:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    Model::preventLazyLoading();
}

A lazy loading nem befolyásolja az alkalmazás helyességét, csak annak teljesítményét. 

Ideális esetben az összes szükséges relációt eager load-oljuk, de ha nem, akkor egyszerűen lazy load-olja a szükséges kapcsolatokat.

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Prevent lazy loading always.
    Model::preventLazyLoading();

    // But in production, log the violation instead of throwing an exception.
    if ($this->app->isProduction()) {
        Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
            $class = get_class($model);

            info("Attempted to lazy load [{$relation}] on model [{$class}].");
        });
    }
}

Részben/teljesen hidratált

Szinte minden SQL-ről szóló könyvben a teljesítményre vonatkozó ajánlások egyike az, hogy „csak azokat az oszlopokat válassza ki, amelyekre szüksége van”. Ez jó tanács! Csak azt szeretné, hogy az adatbázis lekérje és visszaadja azokat az adatokat, amelyeket ténylegesen használni fog, mert minden mást egyszerűen eldob.

Egészen a közelmúltig ez egy trükkös (és néha veszélyes!) ajánlás volt, amelyet be kell tartani Laravelben.

A Laravel beszédes modelljei az aktív rekordminta megvalósítása, ahol a modell minden példányát egy sor támogatja az adatbázisban.

Az 1-es azonosítójú felhasználó lekéréséhez használhatja az Eloquent User::find() metódust, amely a következő SQL lekérdezést futtatja:

$user = User::find(1);
// -> SELECT * FROM users where id = 1;

// Fully hydrated model, every column is present as an attribute.

// App\User {#5522
//   id: 1,
//   name: "Aaron",
//   email: "aaron@example.com",
//   is_admin: 0,
//   is_blocked: 0,
//   created_at: "1989-02-14 08:43:00",
//   updated_at: "2022-10-19 12:45:12",
// }

Ebben az esetben az összes oszlop kiválasztása valószínűleg rendben van! Ha azonban a felhasználói táblázat rendkívül széles, LONGTEXT vagy BLOB oszlopokat tartalmaz, vagy több száz vagy több ezer sort választ ki, akkor valószínűleg csak azokra az oszlopokra szeretné korlátozni, amelyeket használni szeretne.

A kiválasztási módszerrel szabályozhatja, hogy mely oszlopok legyenek kiválasztva, ami részlegesen hidratált modellhez vezet. A memórián belüli modell attribútumok egy részhalmazát tartalmazza az adatbázisban lévő sorból.

$user = User::select('id', 'name')->find(1);
// -> SELECT id, name FROM users where id = 1;

// Partially hydrated model, only some attributes are present.
// App\User {
//   id: 1,
//   name: "Aaron",
// }

Itt válnak veszélyessé a dolgok.

Ha olyan attribútumot ér el, amelyet nem választott ki az adatbázisból, a Laravel egyszerűen nullát ad vissza. A kód azt fogja gondolni, hogy egy attribútum null, de valójában nem választották ki az adatbázisból. Lehet, hogy egyáltalán nem nulla!

A következő példában egy modell részben hidratált csak azonosítóval és névvel, majd az is_blocked attribútum lejjebb érhető el. Mivel az is_blocked soha nem lett kiválasztva az adatbázisból, az attribútum értéke mindig null lesz, és minden blokkolt felhasználót úgy kezel, mintha nem lenne blokkolva.

// Partially hydrate a model.
$user = User::select('id', 'name')->find(1);

// is_blocked was not selected! It will always be `null`.
if ($user->is_blocked) {
    throw new \Illuminate\Auth\Access\AuthorizationException;
}

Lehetséges veszélyek

  • Adatvesztés
  • Adatok felülírása
  • Az ingyenes felhasználók fizetősként kezelése
  • Fizetős felhasználók ingyenesként kezelése
  • Hibás e-mailek küldése
use Illuminate\Database\Eloquent\Model;

public function boot()
{
    Model::preventAccessingMissingAttributes();
}

Mass assignment védelem

A tömeges hozzárendelés egy biztonsági rés, amely lehetővé teszi a felhasználók számára, hogy olyan attribútumokat állítsanak be, amelyeket nem szabad beállítaniuk.

Például, ha rendelkezik egy is_admin tulajdonsággal, akkor nem szeretné, hogy a felhasználók önkényesen frissíthessék magukat rendszergazdává! A Laravel ezt alapértelmezés szerint megakadályozza, és megköveteli, hogy kifejezetten engedélyezze az attribútumok tömeges hozzárendelését.

Ebben a példában az egyetlen attribútum, amely tömegesen rendelhető hozzá, a név és az e-mail cím.

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
    ];
}

Nem számít, hány attribútumot ad át a modell létrehozásakor vagy mentésekor. Csak a név és az e-mail cím mentésre kerül:

User::find(1)->update([
    'name' => 'Aaron',
    'email' => 'aaron@example.com',
    'is_admin' => true
]);

A megoldás: Model::preventSilentlyDiscardingAttributes() metódus.

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Warn us when we try to set an unfillable property.
    Model::preventSilentlyDiscardingAttributes();
}

Strict mód

A Laravel 9.35.0 egy Model::shouldBeStrict() nevű segédmetódussal rendelkezik, amely a három beszédes „strict” beállítást vezérli:

  • Model::preventLazyLoading()
  • Model::preventSilentlyDiscardingAttributes()
  • Model::preventsAccessingMissingAttributes()

Az ötlet az, hogy behelyezheti a shouldBeStrict() hívást az AppServiceProviderbe, és egyetlen metódushívással be- vagy kikapcsolhatja mindhárom beállítást. Röviden összefoglaljuk az egyes beállításokra vonatkozó javaslatainkat:

preventLazyLoading: Elsősorban az alkalmazás teljesítményéért felel. Kikapcsolva production-ban, be lokálisan. (Hacsak nem naplózza a figyelmeztetéseket production-ban.)

preventSilentlyDiscardingAttributes: Elsősorban az alkalmazás helyességéért felel. Mindenhol be kell kapcsolni.

preventsAccessingMissingAttributes: Elsősorban az alkalmazás helyességéért felel. Mindenhol be kell kapcsolni.

Ha nem tervezi a lazy loading naplózását (ami ésszerű döntés!), akkor a következőképpen konfigurálja a beállításokat:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    Model::preventLazyLoading(!$this->app->isProduction());

    Model::preventAccessingMissingAttributes();
    Model::preventSilentlyDiscardingAttributes();
}
Budai Zsolt

Budai Zsolt

Fejlesztő