Tehnologija Vodič

unserialize() – šta radi i kako ga bezbedno koristiti u PHP aplikacijama

unserialize

Kad radite u PHP-u, često možete da naiđete na situacije gde treba da sačuvate kompleksne strukture podataka: nizove, objekte, podešavanja korisnika i sl. PHP za tu namenu nudi jednostavan mehanizam za to: serialize() pretvori promenljivu u tekst, a unserialize() taj tekst vrati nazad u originalni oblik. To je praktično i brzo, ali baš zato što unserialize() može da rekonstruiše objekte, postaje potencijalno rizičan kada se koristi nad podacima koji dolaze spolja.

Ukratko: unserialize() je zgodan alat koji vam štedi posao, ali ga treba koristiti pažljivo. Ovo naročito važi za web aplikacije koje primaju input od korisnika, iz kolačića, iz URL-a ili od nekog eksternog servisa.

Šta unserialize() radi i gde se obično sreće

unserialize() je funkcija koja rekonstruše promenljivu iz njenog serijalizovanog oblika. Serijalizacija je proces u kojem PHP niz, objekat ili kompleksna struktura postaje običan string, koji se zatim lako čuva u fajlu, bazi podataka, kešu ili kolačiću. Kada je potrebno vratiti originalni objekat, poziva se unserialize().

Najčešće situacije gde se koristi:

  • kada aplikacija čuva kompleksna podešavanja u bazi (posebno stariji PHP projekti),
  • u sesijama kada se podaci o korisniku smeštaju kao objekti,
  • u kešu (cache) ako se podaci serijalizuju radi bržeg učitavanja,
  • u kolačićima (često loša praksa, ali česta u starijim sistemima),
  • prilikom komunikacije između dva sistema gde se razmenjuje serialized payload.

U modernim rešenjima to je sve ređe jer se JSON pokazao kao praktičnije i sigurnije rešenje, ali mnogo postojećeg koda i dalje koristi serijalizaciju objekata.

Primera radi, tipičan legitiman primer je čuvanje korisničkih podešavanja:

$userSettings = [

    'theme' => 'dark',

    'language' => 'sr',

    'widgets' => ['stats', 'chart', 'notifications'],

];

$serialized = serialize($userSettings);

// upis u bazu

// kasnije:

$settings = unserialize($row['settings']);

Ovo je potpuno legitiman scenario, sve dok podatak dolazi iz vašeg sistema i niko spolja ga ne menja. Ako isti string neko može da pošalje u URL, kolačić ili POST telu, tada rizik postaje realan.

Rizik: PHP Object Injection (POI). Kako izgleda napad i zašto je opasan

Suština PHP Object Injection-a je da napadač sastavi serijalizovan string koji, kad ga PHP deserializuje, rekonstruiše objekat željene klase. Ako u toj klasi postoje magične metode koje rade osetljive stvari, ako što je pisanje fajlova, brisanje, izvršavanje komandi, napadač može izgraditi serijalizovani payload koji aktivira takvo ponašanje. To se često naziva gadget-lanac (gadget chain) i alate poput PHPGGC koriste oni koji testiraju ili zloupotrebljavaju sisteme.

Magične metode kao što su __wakeup(), __destruct(), __toString(), __call() ili __invoke() mogu biti iskorišćene da se automatski izvrši neželjeni kod. Dovoljno je da postoji i jedna ranjiva klasa u projektu koja u jednoj od navedenih metoda radi IO ili izvršava sistemske pozive, a napad može prerasti u čitanje/pisanje fajlova, upload webshell-a, brisanje podataka ili čak RCE (remote code execution).

Primer jednostavne ranjivosti:

class Logger {

    public function log($message) {

        file_put_contents('log.txt', $message . PHP_EOL, FILE_APPEND);

    }

}

if (isset($_GET['data'])) {

    // ranjivo: unserialize nad podacima koje kontroliše korisnik

    $userSettings = unserialize($_GET['data']);

    echo "Tema: " . $userSettings->theme;

}

Napadač može poslati payload koji instancira Logger (ili neku drugu opasnu klasu) i time izazvati neželjene efekte. URL sa payload-om može izgledati ovako:

http://nekisajt.rs/vulnerable.php?data=O:6:"Logger":0:{}

Alati poput PHPGGC dodatno olakšavaju pravljenje složenih gadget-lanaca ciljanih na poznate biblioteke (Monolog, SwiftMailer, Doctrine…), pa i jedna ranjiva biblioteka u zavisnostima može postati ulazna tačka.

Šta napadač može postići:

  • brisanje fajlova (unlink()),
  • pisanje proizvoljnog sadržaja na disk (file_put_contents()),
  • eksfiltraciju podataka,
  • izvršenje sistemskih komandi (exec, system),
  • preuzimanje servera u najgorim slučajevima.

Realan primer iz aplikacije (ilustracija POI-ja)

Zamislite aplikaciju koja čuva korisnička podešavanja u serijalizovanom obliku i pri učitavanju jednostavno radi unserialize() nad vrednošću iz GET parametra. Kod može izgledati ovako:

class UserSettings {

    public $theme;

    public $language;

}

class Logger {

    public function log($message) {

        file_put_contents('log.txt', $message . PHP_EOL, FILE_APPEND);

    }

}

if (isset($_GET['data'])) {

    // ranjivo

    $userSettings = unserialize($_GET['data']);

    echo "User theme: " . $userSettings->theme;

    echo "User language: " . $userSettings->language;

}

Napadač može da pošalje serijalizovan objekat koji uključuje i Logger. Kad PHP izvrši unserialize(), on rekonstruiše Logger i može eventualno da pokrene funkcije koje Logger ima (u zavisnosti od implementacije magičnih metoda).

Praktican primer zlonamernog payload-a:

$maliciousPayload = serialize([

    new Logger(),

    'theme' => 'dark',

    'language' => 'en'

]);

echo $maliciousPayload;

Kada se to pošalje aplikaciji, PHP rekonstruiše objekat Logger. Ako Logger ima neprikladno implementirane magične metode koje izvršavaju IO operacije bez provere konteksta, napad je realizovan.

Kako se zaštititi — praktične i konkretne preporuke

U nastavku su preporuke koje možete odmah primeniti, sa primerima koda, razlozima i mogućim zamenama. Fokusiraćemo se na praktične korake i kako ih primeniti.

1) Ne unserijalizujte nepouzdane podatke — koristite JSON

Ako podatak dolazi iz spoljnog izvora, ne deserializujte ga. Radije koristite json_encode() / json_decode(..., true):

$json = json_encode($settings);

$settings = json_decode($json, true);

JSON ne instancira klase i ne aktivira magične metode, pa značajno smanjuje rizik.

Da biste testirali ovo pošaljite zlonameran serialized string kao GET parametar. Aplikacija ne bi trebalo da ga deserializuje i ne bi trebalo da se ponaša nekorektno.

Imajte u vidu da JSON gubi informacije o konkretnim klasama; ako vam stvarno trebaju instance, mapirajte ručno (videti DTO dole).

2) Koristite allowed_classes parametar (PHP 7+)

Ako morate da pozivate unserialize() nad spoljnim podacima, ograničite koje klase PHP sme da instancira:

// zabrani instanciranje bilo koje klase

$data = unserialize($payload, ['allowed_classes' => false]);

// ili dozvoli samo određene klase

$data = unserialize($payload, ['allowed_classes' => ['UserSettings']]);

Da biste ovo testirali pošaljite payload sa nepoželjnom klasom. Rezultat treba da bude false ili siguran izuzetak, bez instanciranja zlonamernih klasa.

Imajte u vidu da ako neka biblioteka očekuje interne klase, morate pažljivo da odaberete šta dozvoljavate.

3) Centralizujte deserializaciju (safe_unserialize() wrapper)

Napravite funkciju koja radi sve provere: veličinu payload-a, HMAC verifikaciju (ako je primenljivo), allowed_classes, i logging neuspelih pokušaja. To omogućava da politiku menjate na jednom mestu.

Primer wrappera:

function safe_unserialize(string $payload, array $opts = []) {

    if (strlen($payload) > 20000) {

        throw new RuntimeException('Payload prevelik');

    }

    if (!empty($opts['hmac_secret'])) {

        if (empty($_POST['sig']) || !hash_equals(hash_hmac('sha256', $payload, $opts['hmac_secret']), $_POST['sig'])) {

            throw new RuntimeException('Neispravan potpis');

        }

    }

    $allowed = $opts['allowed_classes'] ?? false;

    $result = @unserialize($payload, ['allowed_classes' => $allowed]);

    if ($result === false && $payload !== serialize(false)) {

        error_log('Neuspešan unserialize: ' . substr($payload, 0, 300));

        throw new RuntimeException('Nevalidan serialized payload');

    }

    return $result;

}

Upotrebom wrappera svi pozivi unserialize() prolaze kroz iste bezbednosne provere.

4) HMAC/potpisivanje payload-ova za poznate servise

Ako primate serijalizovane podatke od poznatih partnera, odnosno servisa, zahtevajte HMAC potpis. Samo nakon verifikacije potpisa, dozvolite deserializaciju.

Slanje:

$payload = serialize($data);

$sig = hash_hmac('sha256', $payload, $secret);

send(['payload' => $payload, 'sig' => $sig]);

Pri prijemu:

if (!hash_equals(hash_hmac('sha256', $_POST['payload'], $secret), $_POST['sig'])) {

    throw new RuntimeException('Invalid signature');

}

$data = unserialize($_POST['payload'], ['allowed_classes' => false]);

Imajte u vidu da tajni ključ mora biti bezbedno čuvan (env var, vault). Ako procuri, sigurnost potpisa pada.

5) Osnovne sintaktičke i veličinske provere

Pre nego što zovete unserialize(), odbacite očigledno pogrešne ili predugačke stringove. Ovo nije potpuna zaštita, ali smanjuje šum i mogućnost uspešnih napada sa velikim gadget-lančevima.

Primer:

if (!is_string($payload) || strlen($payload) > 10000) {

    throw new RuntimeException('Nepodoban unos');

}

if (!preg_match('/^[aOsidbN;:{}"0-9a-zA-Z_,]+$/', $payload)) {

    throw new RuntimeException('Sumnjiv serialized string');

}

Imajte u vidu da regex nije savršeno rešenje, ali može da služi kao dodatna linija odbrane.

6) Pregledajte magične metode u projektu i refaktorišite opasne operacije

Pretražite repo za __wakeup, __destruct, __toString, __call, __invoke. Ako u tim metodama postoje IO operacije, exec, unlink ili file_put_contents, refaktorišite te metode da ne izvršavaju opasne operacije automatski. Radije koristite eksplicitne metode poput destroy() koje zahtevaju autorizaciju.

Primer refaktorisanja:

public function destroy() {

    if (!$this->authorized) {

        throw new RuntimeException('Not authorized');

    }

    unlink($this->path);

}

7) Ne stavljajte pune objekte u sesiju i kolačiće

Izbegavajte serijalizaciju kompletnog Eloquent modela u sesiju. Čuvajte samo user_id i token, a modele učitavajte po id kad su potrebni. Keširajte toArray() ili JSON umesto objekata.

Imajte u vidu da Laravel sesije po defaultu serializuju podatke, pa zato zbegavajte dodavanje modela direktno u sesiju.

8) Migracija serialize -> JSON

Ako imate mnogo starih serijalizovanih podataka u bazi, dodajte novu kolonu (npr. settings_json) i izvedite batch konverziju. Aplikacija treba da prvo proveri JSON kolonu, pa ako ne postoji, da padne na staru serialized kolonu. Time ostavljate rollback putanju i vreme da ručno pregledate neuspešne zapise.

Batch primer:

$rows = $pdo->query("SELECT id, settings_serialized FROM users")->fetchAll();

foreach ($rows as $row) {

    $s = $row['settings_serialized'];

    $arr = @unserialize($s);

    if ($arr === false && $s !== serialize(false)) {

        // log i preskoči

        continue;

    }

    $json = json_encode($arr, JSON_THROW_ON_ERROR);

    $stmt = $pdo->prepare("UPDATE users SET settings_json = ? WHERE id = ?");

    $stmt->execute([$json, $row['id']]);

}

9) Monitoring, logovanje i WAF

Logujte svaki neuspešan unserialize() pokušaj, sa preview-em payload-a i IP adresom. Postavite alert koji detektuje nagli porast zahteva koji sadrže obrasce serijalizovanih objekata (regex O:\d+:“). WAF (ModSecurity, cloud WAF) može da blokira i filtrira sumnjive serialized parametre.

Incident response: blokirajte IP, stavite aplikaciju u maintenance mode, analizirajte logove i payload-ove, vratite iz bekapa ako je potrebno, rotirajte tajne ključeve.

10) Laravel specifično — session, cache, artisan alati

  • Ne stavljajte Eloquent modele u sesiju.
  • Keširajte JSON i toArray() umesto objekata.
  • Dodajte artisan komandu koja skenira repo za unserialize() i pravi izveštaj za audit.
  • Dodajte middleware koji proverava veličine i formate parametara na rutama koje očekuju korisnički input.

Dodatne tehnike za dodatnu sigurnost i hardening

Ako ste već uveli wrapper i allowed_classes, sledeći slojevi odbrane dodatno smanjuju površinu napada i blast radius. Ovo su tehnike koje možete uvesti paralelno sa prethodnim merama.

DTO pattern umesto direktne deserializacije objekata

Prihvatite podatke kao niz (preferirano JSON), validirajte ih i mapirajte na DTO. Na taj način eksplicitno kontrolišete polja i tipove.

Primera radi:

class UserSettingsDto {

    public string $theme;

    public string $language;

    /** @var string[] */

    public array $widgets;

}

$json = file_get_contents('php://input');

$data = json_decode($json, true);

if (!isset($data['theme'], $data['language'], $data['widgets']) || !is_array($data['widgets'])) {

    throw new RuntimeException('Neispravan payload');

}

$dto = new UserSettingsDto();

$dto->theme = (string) $data['theme'];

$dto->language = (string) $data['language'];

$dto->widgets = array_map('strval', $data['widgets']);

Ovo uklanja potrebu za unserialize() u toku obrade spoljnjeg inputa.

Biblioteke/serializer-i sa whitelistom svojstava

Ako koristite biblioteke za automatsku serijalizaciju/deserializaciju (npr. Symfony Serializer), konfigurišite ih da mapiraju samo eksplicitno dozvoljena svojstva:

$allowedProperties = ['theme', 'language', 'widgets'];

$object = $serializer->denormalize($data, UserSettingsDto::class, null, ['attributes' => $allowedProperties]);

Time sprečavate da serializer popuni privatna ili neočekivana svojstva.

Smanjite privilegije okruženja

Smanjite blast radius tako što ćete ograničiti šta web proces sme da radi:

  • fajlovi i direktorijumi pod pravim vlasništvom i ograničenim permisijama,
  • open_basedir za PHP da ograniči pristup fajl sistemu,
  • disable_functions u php.ini (isključiti exec, system, shell_exec i sl. ako nisu potrebni),
  • stroga kontrola lokacije upload-a i tipova fajlova.

Statička analiza i skeniranje dependencija

Uvedite PHPStan/Psalm sa pravilima koja detektuju pozive unserialize() i korišćenje opasnih funkcija. Koristite composer audit, Snyk ili Dependabot da pratite ranjivosti u zavisnostima. Automatizujte CI da failuje build ako se pojavi neodobren unserialize() poziv.

Runtime detekcija i honeytoken mehanizmi

Postavite „canary“ polja (npr. skriven kolačić ili polje) čije menjanje signalizira manipulaciju i izaziva alarm ili blokadu. Implementirajte rate limiting i WAF pravila za rute koje očekuju serialized parametre.

Dizajn koji eliminiše potrebu za deserializacijom objekata

Kreirajte aplikaciju koja koristi primitive i identifikatore kroz transport i tek onda eksplicitno učitava objekte iz baze. To znači manje potrebe za unserialize() i jasniji, kontrolisan tok podataka.

Incident response plan i backup strategija

Imati plan: kako izolovati aplikaciju, kako vratiti iz bekapa, kako rotirati ključeve (HMAC), kako analizirati logove. Testiran i dokumentovan postupak vraća sistem u rad brže i sa manjom štetom.

Praktični kod primeri i saveti za hitne slučajeve

Whitelist svojstava pre mape (property-filtering):

$allowed = ['theme', 'language', 'widgets'];

$input = json_decode($json, true);

$filtered = array_intersect_key($input, array_flip($allowed));

Revocable token za serijalizovane payload-ove (jednokratna upotreba):

  • pri kreiranju payload-a zabeležite token u bazi sa statusom „unused“ i vremenskim žigom,
  • po prijemu, proverite token i odmah ga označite kao iskorišćen,
  • tako se sprečavaju replay napadi.

Sandbox test okruženje za eksploatacione pokušaje:

  • koristite lokalni Docker sandbox koji reprodukuje vašu aplikaciju,
  • izvođenje PHPGGC i drugih test payload-ova radite isključivo lokalno, nikad na produkciji.

Detekcija ranjivosti u kodu — šta da pretražite odmah

  • grep -R "unserialize()" . — pregledajte sve pojave u repozitorijumu.
  • Pretražite gde se u kolačiće, session ili cache pišu serialized objekti.
  • Pretražite magične metode (__wakeup, __destruct, __toString itd.).
  • Proverite third-party biblioteke na listama gadget-lanaca (PHPGGC).

Kratka kontrolna lista (checklist) za audit

  • Pretražili ste repo za unserialize() i serialize() pozive?
  • Sve pozicije gde unserialize() postoji su zaštićene wrapperom, allowed_classes ili potpisom?
  • Ne čuvate serialized objekte u kolačićima?
  • Sesije i cache ne sadrže neproverene objekte?
  • Postavljen je logging za neuspešne unserialize() pozive?
  • Postavljen plan migracije serialize -> JSON gde je moguće?
  • Imate WAF pravila i alerting za sumnjive obrasce?

Zaključak

Kao što ste videli, unserialize() sama po sebi nije nikako zlonamerna funkcija. Naprotiv, korisna je i postojala je godinama sa razlogom.

Problem nastaje kad se koristi bez razmišljanja nad podacima koje korisnik može menjati. Stoga je najvažnije da primenite višeslojnu odbranu: migrate na JSON gde je moguće, koristite allowed_classes, centralizujte deserializaciju, zahtevajte potpisivanje od partnera, refaktorišite magične metode, minimalizujte privilegije okruženja i uvedite monitoring i plan za incident.

Male, dosledne promene danas vraćaju se višestruko u izbegnutim incidentima, očuvanim podacima i mirnijem radu produkcije.

Tagovi:

php

Ostavi komentar

Vaša adresa neće biti objavljena