V souvislosti s rozšířením spamovacích robotů a technik typu „session riding“ je stále častěji potřeba mít jednoduchý generátor unikátních identifikátorů, který splňuje jisté požadavky. Takový generátor v jazyce PHP vám nabízím.
Princip zabezpečení formulářů pomocí unikátního identifikátoru (dále ID) je prostý: Do formuláře je vložen při jeho generování na straně serveru jednoznačný řetězec, který si zároveň server poznamená jako „vydaný“. Pokud přijde odeslaný formulář, zkontroluje server tento řetězec a ověří, že už byl vydán a že zároveň nebyl použit. Pokud tyto požadavky splňuje, označí jej server jako použitý a data přijme. Pokud řetězec nevyhovuje, server data odmítne.
Podobný princip lze použít i pro AJAXové požadavky.
Požadavky na generátor
Jako své požadavky na generátor takových ID jsem určil tyto:
- 1. Nebudou vydány dva stejné ID v jeden čas
- 2. Vydaný ID lze použít k poslání dat pouze jednou
- 3. Jednou použitý ID nebude vydán nikdy v budoucnu
- 4. Vydaný ID lze použít pouze z té adresy, pro níž byl vydán
- 5. Platnost vydaného ID je zajištěna pouze po předem stanovenou dobu, po jejím uplynutí už není použitelnost vydaného ID zaručena
- 6. Nelze použít ID, který nebyl serverem vydán
Řešení
Knihovna uniqID generuje identifikátory založené na aktuálním čase (časové razítko + mikrosekundy), tím splňuje požadavek 3. Ke splnění požadavku 1 je ke každému ID přidáno náhodné šestimístné číslo. Existuje tedy nenulová pravděpodobnost, že budou ve stejný časový okamžik vygenerovány dva identifikátory a bude jim přidáno stejné náhodné číslo, ale pravděpodobnost je natolik malá, že ji možno pro tyto účely zanedbat.
Po vygenerování identifikátoru si server vytvoří v pracovním adresáři soubor se stejným jménem. Při kontrole vydaného uniqID se kontroluje existence tohoto souboru (požadavek 6) a pokud existuje, je smazán (požadavek 2). Jako platný je uniqID vyhodnocen pouze tehdy, když se podařilo smazat soubor.
Požadavek ad 4 je zajištěn tím, že do výše zmíněného souboru si server při jeho generování poznamená část IP adresy klienta. Požadavek ad 4 tak není splněn zcela, na druhou stranu je tímto způsobem ošetřena většina případů, kdy je klient, např. s vytáčeným spojením, mezi vygenerováním a použitím uniqID odpojen a znovu připojen - počítá se s tím, že se změní jen část IP.
Požadavek ad 5 je řešen CRONem, který v pravidelných intervalech prochází seznam vydaných uniqID a maže ty, které jsou starší než určený čas.
Vygenerované identifikátory jsou před použitím ošetřeny pomocí jednoduchého zakódování, kdy je jednoduchý číselný řetězec, vzniklý při generování, upraven tím, že jsou jednotlivé číslice nahrazeny různými písmeny (jedna číslice může být zakódována několika různými písmeny) a jejich pořadí je změněno. Před kontrolou je řetězec opět dekódován. Konkrétní přiřazení znaků k číslicím, stejně jako konkrétní změny pořadí, je možno individuálně nastavit. Knihovna obsahuje nástroj pro generování náhodných šifrovacích informací. Tento nástroj je dostupný i online: uniqGEN.
Použití knihovny
Stáhněte si soubor uniq.php
. Na řádku define('UNIQ_PATH','*******',1); vyplňte cestu k adresáři, v němž budou uloženy soubory s vydanými identifikátory. Skript musí mít právo do tohoto adresáře zapisovat.
Pomocí nástroje uniqGEN (PHP skript uniqgen.php
) si vygenerujte informace pro šifrování ID. Dva vygenerované řádky vložte na označené místo ve skriptu uniq.php.
Pokud chcete, můžete na řádku define('UNIQ_TTL','3600',1); změnit dobu platnosti vydaných identifikátorů (v sekundách).
Skript uniq.php nahrajte na server a includujte jej do svých skriptů. Použití je prosté a je vidět v souboru index.php
. UniqID je 26znakový řetězec a je vrácen jako návratová hodnota při volání funkce getuniq(). Platnost uniqID, který skript obdržel od klienta, lze pak ověřit voláním funkce checkuniq($uniqid), jejímž parametrem je předaný identifikátor a návratová hodnota je buď 1, pokud je identifikátor vyhodnocen jako platný, nebo 0, pokud došlo k libovolné chybě.
Ke korektní funkci knihovny je potřeba zajistit občasné volání funkce purgeuniq(). Nejjednodušší způsob je využít k tomu účelu připravený skript cron.php
. Pokud vaše aplikace řeší pravidelné úlohy jinak, můžete použít vlastní způsob.
Download
Knihovna je dostupná pod licencí MIT. Na Google Code je pod názvem uniqid-php
. Můžete si ji stáhnout i pomocí Subversion (svn checkout http://uniqid-php.googlecode.com/svn/trunk/ uniqid-php) či přímo ze Subversion repository
. (viz též Jak začít pracovat s Google Code
)

Chyby
Proč pro splnění požadavků 1 a 3 není použita funkce uniqid? Ta se zároveň stará i o unikátnost.
Doufám, že náhrada číslic písmeny není míněna jako seriózní zvýšení bezpečnosti, ale jenom jako taková legrácka. Vzhledem k tomu, že část ID je odvozena z času, který se mění jen velmi pomalu, tak jde během několika pokusů přijít na to, kterým číslům odpovídají která písmena.
Aby skript fungoval tak, jak slibuje, tak by se funkce purgeuniq() musela volat každou sekundu. Kontrola času by měla být součástí přístupu k ID a promazání by se použilo jen pro staré záznamy, ke kterým už nikdo nepřistupuje.
Chyby a selhání!
1. Jen mi není jasné, jak s funkcí uniqid() jednoduchým způsobem zařídit časové omezení vydaného ID a promazávání vydaných. Leda si flag soubory generovat jako time().uniqid()... Ale no prosím, i tak lze, já zvolil jiný způsob.
2. Náhrada číslic písmeny spolu s proházením jejich pořadí je pouhé maskování, aby z výsledku netrčel tak okatě onen timestamp. Ale já mám rád podobné frajerské výzvy, tak prosím: http://contrib.dev20.info/uniqid/demo.php - můžete si udělat oněch několik pokusů a napište mi, jaké číslici odpovídá jaký znak, ano? Díky.
3. Kontrola času při kontrole ID není prioritou. Nejde o to, aby nějaké ID fungovalo v čase T+3599 a v čase T+3600 už ne, jde o to, aby po čase T+3600 bylo jasné, že „teď už to fungovat nemusí“.
Martin Malý
Řešení
1. Čas souboru lze zjistit pomocí filemtime. Nicméně jistou eleganci (a vyšší rychlost) v uložení do názvu souboru spatřuji. Z uniqid lze čas zjistit velice snadno, protože vrací něco jako
sprintf("%s%08x%05x", prefix, sec, usec). A jak se zajišťuje unikátnost? Pomocíusleep(1)- stejně by se to ostatně dalo udělat i v PHP.2. Mezi jednotlivými ročníky podzimní soutěže Crypto-World to bylo příjemné jarní zpestření, svojí složitostí by se to ale asi zařadilo do nejlehčí kategorie ;-). Řešení je
*****. Řešení je správné, ale - nechť si mohou i další lámat hlavu! Pozn. MM3. Nicméně pokud pročišťovátko budu spouštět jednou za hodinu, tak to, že délku platnosti nastavím na půl hodiny, mi nepomůže, protože stejně může zůstat v platnosti téměř hodinu a půl. Opravdu by bylo rozumné čas kontrolovat i při přístupu nebo alespoň uvést, že k tomu nastavenému času se přičítá doba do spuštění pročištění.
Dobrý!
1. Lze samozřejmě zjistit takto čas, o tom žádná. Ale z nějakého důvodu jsem zvolil způsob, kde se čas ukládá přímo do názvu... Jo, už vím! Proto, abych nemusel volat zbytečně filemtime... :)
2. Dobrý! Klobouk dolů... Kolik vzorků bylo potřeba? A co třeba postup pro zajímavost naznačit? Díky...
3. Ano, taknějak jsem nepociťoval potřebu zdůrazňovat, že interval pročišťování by měl být kratší než životnost.
Kontrola času při přístupu je ale rozumný nápad, tak jsem ji tam dodělal, díky... Pročišťovátko tak zůstává na nepoužité mrtvolky a může být voláno třeba jednou denně, čistě kvůli místu na disku.
Martin Malý
Postup
Použil jsem čtrnáct vzorků, ale dalo by se to udělat i s mnohem menším množstvím. Dotazy jsem totiž nekladl cíleně, ale náhodně - na začátku jsem položil pár dotazů, zanalyzoval je, po nějaké době položil další dotazy (spíš jen proto, abych se dozvěděl, na kterých pozicích je která část timestampu, což nezbytně nutné nebylo). Pak mi chybělo několik čísel, tak jsem položil několik dalších dotazů - zase jsem nečekal na ten správný okamžik, ale poslal jsem jich několik, dokud v nich to číslo nebylo.
Řešení vyšlo na 16 řádků PHP kódu a trochu ruční práce v Excelu. Nejprve jsem podle počtu různých znaků na jednotlivých pozicích zjistil, které znaky jsou náhodné a které pocházejí z timestampu. Ty, které se mezi jednotlivými dotazy měnily, odpovídaly patřičné části timestampu - když byl mezi dotazy rozdíl jednu sekundu, změnila se jedna nenáhodná pozice. Z několika dotazů jsem zjistil, které části ID odpovídají které části timestampu a na základě toho přiřadil jednotlivým číslům řetězce (aktuální hodnotu timestampu vrací přímo server).
Když jsem teď dokázal, že to žádný praktický přínos nemá, tak to z kódu vyhodíš? ;-)
Díky
Díky za popis.
V kódu to buď nechám jako památníček, ono to účel (=maskování) plní, nebo to nahradím nějakým jiným maskováním... base64 třeba...
Dobře, dobře, dám tam AES! :)
Martin Malý