PDA

Zobacz pełną wersję : [session] Programowanie niekoniecznie pod Joomla :)



Rybik
11-07-2008, 14:01
Przeglądając internet w poszukiwaniu kilku prostych spraw zauważyłem, że "programowanie pod CMS" ogłupia a właściwie rozleniwia i demoralizuje. Nie byłoby to zjawisko groźne, gdyby nie pęd młodych programistów do dzielenia się ... niewiedzą.
W związku z powyższym, oraz w odpowiedzi na kilka prywatnych wiadomości i w ramach szeroko pojętej autopromocji napisze conieco o sesjach w php.
Sesja / sesja logowania - to zestaw danych pamiętanych przez php wyłącznie dla danego użytkownika, na określony czas. Ale po co sesja ?

Zmienne
Zwykła zmienna $foo zostanie zapomniana po zakończeniu skryptu. Co zwykle oznacza 'natychmiast po wygenerowaniu strony'. Więc nie sposób tak zapamiętać, że pan Jan Przykładowy 3 strony wstecz wybrał opcję 'kawa z mlekiem'.

Numer IP
Co z IP ? Pan Jan Przykładowy może współdzielić numer IP z sąsiadami, lub
doświadczyć dynamicznej zmiany IP w neostradzie, poza tym wprowadzenie zarządzania ustawieniami użytkowników na podstawie IP jest troche jak Fiat Multipla - obszerne i budzące konsternację

GET/POST
Co z $_POST i tymi dopiskami do adresu w stylu index.php?ktopuka=jasiek (mowa o $_GET). Bardzo to piękne ale taka 'pamięć' po pierwsze ma mechanikę umysłu kota (pamięta tylko ostatnie zdarzenie*) a po drugie nie daje nam gwarancji, że to cały czas ten sam Jasiek**.
*dane $_POST i $_GET są dostępne tylko na stronie do której się odwołujemy, na kolejnej przepadają
**dopisać coś do adresu może każdy

Sesja
Sesja udostępnia nam pomocną tablicę $_SESSION, do której możemy zapisywać co nam się podoba:

$_SESSION['imie']='Jasiek';
i dane te będą przechowywane przez cały czas ... istnienia sesji (o tym dalej). To otwiera nowe możliwości:
- zapisywanie tam danych z formularza* (GET/POST), jeśli będziemy ich potrzebować później, np. po finalnym zatwierdzeniu formularza, które wspomniane GET i POST wyczyści i bez zanotowania do $_SESSION nie wiedzielibyśmy co właściwie zatwierdza
*WSZYSTKIE DANE Z FORMULARZY : trzeba przepuścić przez htmlspecialchars i/lub wyrażenie regularne sprawdzające, czy dostajemy to co potrzeba a nie jakiś sprytny kod niszczący świat
Jeżeli na serwerze mamy włączoną dyrektywę magic_quotes_gpc to dane z formularzy (a dokładniej wszystkie dane z _GET, _POST i _COOKIE) będą miały wyescape'owane pojedyncze i podwójne cudzysłowy przez dodanie slashy \ (Jack "the\hacker" O'Examplary stanie się Jackiem \"the\\hackerem\" O\'Examplary. Dlatego jeżeli nie spodziewamy się takich slashy to trzeba je wyciąć. Oto przydatna funkcja prefiltrująca:


function prefilter(&$data) {
if(is_array($data))
array_map('prefilter', &$data);
else
$data= htmlspecialchars(stripslashes($data),ENT_QUOTES);
}
// przykład użycia:
prefilter(&$_POST);

Ok, mamy podręczny notesik dla odwiedzającego ale co właściwie powoduje, że notesik wie do kogo należy?
PHP po inicjalizacji sesji funkcją session_start(); zacznie szukać identyfikatora sesji, tzn ciągu znaków NAZWASESJI=IDENTYFIKATOR, gdzie nazwa to zwykle PHPSESSID, chyba, że nazwiemy sami inaczej funkcją session_name('inna_nazwa');
Teraz najwazniejsze - GDZIE szuka identyfikatora ... skąd w końcu wie, że to ten sam Jasiek?
Ano w kolejności przelatuje $_COOKIE , $_GET i $_POST

I co to znaczy ?
$_COOKIE będzie automatycznie zawierać zawartość plików cookie wysłanych przez tą stronę. Jeżeli Jasiek ma przeglądarke z obsługą cookiesów to go rozpoznamy. Wejdzie na stronę, dostanie ciacho i będzie je miał dopóki nie zamknie przeglądarki - chyba, że ustawimy inaczej, zajrzy raz jeszcze lub odwiedzi podstronę -> cookie wyląduje w $_COOKIE, tam php znajdzie poprzedni identyfikator sesji, sprawdzi u siebie czy taka sesja była i przywróci jej zawartość.
$_GET to znaczki w adresie url, Jasiek nie chce ciastek, ale my to przewidzielim i przekierowalim go na stronę index.php?PHPSESSID=1314455 i dopóki klika linki z taka doklejką i uzywa formularzy z taka doklejką (action="index.php?PHPSESSID=1314455" lub ukryte pole formularza z name="PHPSESSID" i value="1314455", to PHP wie, że to ten sam user
$_POST to dane z formularzy używających metody ... POST, tutaj nie musimy doklejać nic do adresu, wystarczy identyczne jak dla $_GET ukryte pole formularza, każdy formularz z takim polem może Jaśka zidentyfikować.
Czas przechowywania "notesu" sesyjnego na serwerze jest do ustawienia, podobnie żywotnośc ciastka. Prosze sobie doczytać :)

Co to wszystko oznacza w praktyce?
1. Sesji używamy kiedy chcemy coś spersonalizować niekoniecznie od razu używając rejestracji i logowania na stronie
- zapamiętać, że wybrał niebieski wariant naszego templaka i powiększone litery
- napisać wielostopniowy formularz bez ukrytych pól
- pamiętać wysępioną od niego datę urodzin lub datę ostatnich odwiedzin
- pamiętać ostatnio czytany artykuł i zaproponować mu go przy kolejnej wizycie
- sesji używamy oczywiście przy rejestracji i logowaniu userów

2. Bezpieczeństwo
- nigdy bezkrytycznie nie zapisujemy danych z GET/POST/COOKIE/SERVER do sesji czy gdziekolwiek indziej - te dane trzeba przefiltrować
- nigdy nie trzymamy w sesji i ciastkach jawnego hasła, jeżeli z jakichś przyczyn (bliżej nie znane) musimy tam przechować hasło to przynajmniej w postaci zakodowanej(md5, sha) i porównujemy zakodowane-zapamiętane z zakodowanym-wpisanym
- jeżeli oglądamy stronę na którą się zalogowaliśmy i widać identyfikator sesji w adresie url, to takiego linka nie należy nikomu wysyłać bez obcięcia identyfikatora ... gdyby ktoś z wyłączoną obsługa ciastek go kliknął zanim sesja na serwerze wygaśnie, zostałby zidentyfikowany jako my ... nie wspomnę już o publikacji takich linków na forach

3. Co zrobić, jeżeli chcemy być ciastkoodporni, czyli niezależni od tego czy user ma włączoną obsługę cookies czy nie.
Jeżeli serwer stwierdzi, że nie da się wysłać ciastka zainicjuje stałą SID, którą:
- możemy (my sami ręcznie) doklejac do linków (tylko do własnych linków!):


<a href="index.php?<?php echo SID;?>">link</a>

- możemy rozbić i wstawić w formularzu


<form method="POST">
... pola formularza
<?php if(SID) {><input type="hidden" name="<?php echo reset(explode('=',SID));?>" value="<?php echo end(explode('=',SID));?>"/><?php }?>
</form>

- możemy wysłać SID przez url (GET) pozostawiając formularz POST'owy


<form action="index.php?<?php echo SID;?>" method="POST">
... pola formularza
</form>

Wyjaśnienia:
reset(explode('=',SID));
zwraca pierwszą warotośc tablicy, po rozbiciu SID względem znaku równości, nie używam indeksów tablicy, bo nie chce mi się najpierw przepisywać rozbitego SID do zmiennej a inaczej indeksów nie użyję
end(explode('=',SID));
j/w tyle, że to ostatni element
Przy włączonych ciastkach SID nie istnieje, pozostaną nieszkodliwe pytajniki, można je równiez przerzucić z htmla do echo i będzie elegancko.
SID jest stałą predefiniowaną co oznacza, że nie trzeba się jej bać. Co prawda id sesji możemy oszukać edytując ciastko ale:
- jak jest ciastko to nie ma SID :)
- jak nie ma ciastka ale podamy kod przez GET to php jak zauważy identyfikator ze znakami innymi niż a-zA-Z0-9 oraz myślnik to wywali błąd i zainicjuje nową sesje z nowym-poprawnym id (oraz SID).


4. Ułatwienia i zagrożenia
php posiada taka opcje jak session.use_trans_sid, która w przypadku zablokowania ciastek:
- automatycznie dopisuje SID do lokalnych linków
- automatycznie dodaje pole formularza z identyfikatorem sesji
- dodaje SID do innych aktywnych elementów (wymienionych w php.ini -> url_rewriter.tags)
przełączanie na czas działania skryptu:

ini_set('session.use_trans_sid',1)
blokowanie rewritera dla botów (google indeksuje pełne linki z PHPSESSID ale SID nie pokazuje, nie wiem jak inne)


if(strpos($_SERVER['HTTP_USER_AGENT'],"google")!==false or strpos($_SERVER['HTTP_USER_AGENT'],"MSIECrawler")!==false)
{
ini_set("url_rewriter.tags","");
}

zapewnienie walidacji XHTML (zmiana & na &)


ini_set('arg_separator.input','&');
ini_set('arg_separator.output','&');


Zagrożenia związane z używaniem trans_sid:
tylko zagrożenia atakami SQLinjection gdy sesje są trzymane w bazie danych i bazodanowy handler sesji nie sprawdza poprawności identyfikatora sesji przed zapisaniem go do bazy. Nie wyjaśnię, bo aż tak daleko mnie nie zaniosło :)