PHP és MySQL adattípus túlcsordulások

Azt ügye tudjuk, hogy a PHP-ben automatikus típuskonverzió van, amire oda kellene figyelni. A PHP-ban a számok ábrázolására 2 típus áll rendelkezésre (+boolean):
– (signed) integer
– (signed) float

Ezek minden esetben előjellel vannak ellátva, így az integer típusban maximum 2^63 érték tárolható egy 64 bites rendszeren, 64 bites PHP-val. Viszont! A MySQL-ben rendelkezésre áll az előjel nélküli változat, az unsigned bigint, ami ugyanezen körülmények között 2^64 tud ábrázolni, előjel nélkül. Így marad egy nagy hézag a kettő között.  Mivel a PHP-ban automatikus típuskonverzió van, így az (unsigned big)int átalakul a PHP-ben float típussá, amiből mi semmit sem veszünk észre. A float típussal ügye csak pontosságot vesztettünk.

De mi van akkor, ha a PHP által adott választ egy szigorúbb nyelvvel akarjuk feldolgozni – vagy mert int-et várunk -, erőltetjük az int típust:

echo (int) ((int) PHP_INT_MAX+1); // -9223372036854775808

Akkor az bizony a PHP-ban is túlcsordul, ahogy azt a MySQL-ben is tenné ha signed bigint lenne és ebből mi semmit sem veszünk észre. Ugyanez, automatikus típuskonverzióval:
echo(PHP_INT_MAX+1); // 9.2233720368548E+18

Hogy ez miért okozhat gondot?

Elterjedt szokás, hogy biztonsági okok miatt lehetőleg csak ID-ket (azonosító számokat) küldünk szerver oldalra, mivel ezeket jóval könnyebb szűrni, mint a stringeket. Az adatbázisban pedig az azonosítók legtöbb esetben integer típusok, leginkább unsigned bigint-ek, autoincrement tulajdonsággal. Később pedig a szerver oldali vizsgálatnál – mivel számot várunk és az autotincrement alapból 1-ről indul – lustaságból ezt alkalmazzuk és úgy gondoljuk, hogy ezzel macera nélkül (prepared statements) meg is oldottuk az SQL injection elleni védelmet:


$id = abs($_GET['id']); // barmi nem szamra 0-t ad, ami az alap autoincerement 1-es kezdosorszama miatt nem letezo sorra mutat az adatbazisban
$results = $pdo->query('UPDATE users SET money=money+1000 WHERE id='.$id);

Ok, de eddig még sohasem okozott gondot a túlcsordulás, miért kellene rá figyelnem? 

Hát például ezért:
echo (int) PHP_INT_MAX; // 9223372036854775807
echo (int) ((PHP_INT_MAX+1000000)-1000000); // 9223372036854774784

Természetesen ez oda-vissza érvényes, azaz figyelni kell arra, hogy a PHP integer típusa több egész szám tárolására képes, mint a MySQL bizonyos integer típusai. A MySQL persze dob warningot, de ha külön nem kérdezzük meg róla, akkor jó eséllyel nem fogjuk észrevenni PHP-ben.

 

Sok forráskódban láttam már a fent említett lustaságból alkalmazott szám-konverziós védelmet. A baj az, hogy úgy hiszik elegendő csupán (int) -el erőltetni a típust. Csak hogy az negatív számokat is elfogad, így a túlcsordítással máris pozitív számmá változtathatjuk, ami viszont már tutira létezik az adatbázisban a tipikus autoincrementes megoldásoknál. Kiszámolni pedig könnyű a megfelelő mértékű túlcsordítást okozó számot, hogy a kért ID-t kapjuk eredményül.