Strona główna
O mnie
Projekty
Publikacje
Linki



 

Sklepy internetowe"
Autor: Adam Major

Wersja do druku
Artykul został pierwotnie opublikowany w magazynie Software 2.0 3/2002 - "Programowanie dla Internetu". Software 2.0
www.software.com.pl


Aby pobrać z sesji informacje o identyfikatorach wszystkich produktach jakie klient ma w koszyku i skonstruować na tej podstawie zapytanie do bazy danych możemy się posłużyć następującą funkcją.
      Zmienna $zap zawiera identyfikatory produktów z koszyka w formacie id1,id2,id3... pozostałą informację o wybranych produktach uzyskamy wydając zapytanie SQL:

select id_pr, name, price_n, price_b, vat from prod where id_pr in ($zap);

W tablicy $qt_tab mamy zapisaną ilość dla poszczególnych identyfikatorów produktów.

      Po zastosowaniu w/w funkcji uzupełnionej o brakujące pobranie stosownych danych z bazy oraz formatowanie wyników zapytania możemy uzyskać taką stronę prezentującą zawartość koszyka.
 
<?php
function list_cart()
{
global $HTTP_SESSION_VARS;

 $stan = $HTTP_SESSION_VARS[cart]->show_cart();
 if (!$stan)
 {
  $HTTP_SESSION_VARS[c_total] = 0;
  info_cart_empty();
  return 0;
 }

 while (list($key, $value) = each($stan))
 {
  if ($key > 0)
  {
   $zap .= $key . ',';
   $qt_tab[$key] = $value;
  }
 }

 $zap_size = strlen($zap);
 if ($zap_size == 0)
 {
  info_cart_empty();
  return 0;
 }
if ($zap[$zap_size-1] == ',')
   $zap = substr ($zap, 0, -1);
//...
}
?>
Listing 7. Pobieranie informacji o identyfikatorach produktów w koszyku.


Rysunek 2. Strona prezentująca zawartość koszyka.

Równie ważne co zaplanowanie bazy danych i sposobu realizacji koszyka jest

Struktura katalogów

Dobrze zaplanowana struktura katalogów pozwala na łatwe zarządzanie plikami (np. zmianę wyglądu itp.) serwisu oraz ewentualną rozbudowę jego funkcjonalności.
      W głównym katalogu serwisu utworzyłem katalogi:
admin, który zawiera wszelkie skrypty potrzebne do działania panelu administracyjnego;
pict zawiera wszystkie obrazki tworzące wygląd serwisu;
tpl przeznaczony jest na szablony dokumentów itp.
Osoby odpowiedzialne za layout sklepu powinny mieć tylko dostęp do katalogów pict i tpl. W katalogu img tworzone są dwa podkatalogi (mini i big) przechowujące zdjęcia produktów, miniaturki umieszczane są w mini, a zdjęcia o normalnej wielkości w big. Nazwy plików powstają na podstawie dwóch pól: identyfikatora produktu oraz typu pliku, zawartego w polach mini_t i big_t.
      Katalog conf przeznaczony jest na przechowywanie pliku(ów) konfiguracyjnych, jeśli istnieje taka możliwość to powinien zostać umieszczony poza DOCUMET_ROOT (katalogiem widocznym przez serwer WWW). Niestety wielu ISP nie daje takiej możliwości, dlatego należy stosować pliki konfiguracyjne z rozszerzeniem PHP (lub innym przetwarzanym przez PHP).
  Rysunek 3. Schemat struktury plików.

Skoro wspomniałem już o plikach konfiguracyjnych rozwińmy, krótko to dość mało ekscytujące zagadnienie.

Plik konfiguracyjny

Warto stworzyć jeden plik konfiguracyjny dla całego sklepu, a następnie go inkludować w skryptach, które muszą skorzystać z danych w nim zawartych. Najważniejszą zasadą tworzenia takiego pliku jest to aby był on możliwie małych rozmiarów i zawierał tylko te informacje, które są przydatne w większości skryptów. Jeśli mamy klika zmiennych konfiguracyjnych używanych w zaledwie w 1 lub 2 plikach warto się pokusić o zrobienie dla nich osobnego pliku konfiguracyjnego.
Cóż więc plik konfiguracyjny powinien zawierać? Na pewno informacje potrzebne do korzystania z bazy danych, ścieżkę do katalogu img, może login i hasło do FTP'a. Osobiście preferuje używanie jako plików konfiguracyjnych zwykłych skryptów PHP zawierających dane w formacie $nazwa_zmiennej = wartosc;

Można co prawda zastosować bardziej elegancki i łatwiejszy w użytkowaniu, rodzaj konfigów, na wzór pliku php.ini. Niestety mimo udostępnienia specjalnej (nieudokumentowanej) funkcji parse_ini_file() ten sposób jest trochę wolniejszy od zwykłego inkludowania. A zmian w konfiguracji sklepu nie dokonuje się w końcu tak często.  
<?
$db["host"] = 'localhost';
$db["base"] = 'nazwa_bazy';
$db["user"] = 'uzytkownik';
$db["pass"] = 'tajne_haslo';
$dir_img = '/home/httpd/html/sklep/img';
?>
Listing 8. Przykład pliku konfiguracyjnego cfg.php

Bezpieczeńśtwo

W tej części artykułu postaram się przedstawić najczęściej spotykane luki w skryptach tego typu oraz sposoby zabezpieczania aplikacji przed złośliwymi (nieuczciwymi) użytkownikami.


Tablice $HTTP_*_VARS

Bardzo zalecane jest stosowanie tablic asocjacyjnych z grupy $HTTP_*_VARS, pozwalają one jednoznacznie stwierdzić skąd dane pochodzą. Od wersji 4.1.0 dostępne są także tablice asocjacyjne (np. $_GET[]), które mają być zamiennikiem tych z rodziny $HTTP_*_VARS. Ich dużą zaletą jest krótszy zapis oraz globalny zasięg (w funkcjach nie trzeba ich deklarować za pomocą global $nazwa_zmiennej).

Część programistów PHP bagatelizuje problem, twierdząc, że skoro jest opcja variables_order w pliku php.ini i zmienne sesji zawsze nadpisują zmienne z innych źródeł (np. z GET) to praktycznie nie ma problemu. Przy źle napisanej aplikacji stosowanie globalnej przestrzeni nazewniczej może doprowadzić do poważnej luki w bezpieczeństwie.
      Aby skutecznie się podszyć pod zmienną sesji trzeba odgadnąć nazwę zmiennej i wartość jaką powinna przyjąć, jednak teoretycznie jest to możliwe. Dużo bardziej widoczny jest ten problem w przypadku formularzy, bardzo łatwo (jeśli nie sprawdzamy skąd zmienne pochodzą) jest udawać za pomocą parametrów URL'a zmienne, które powinny być wysłane metodą POST.
 
// plik1.php
<?php
// jeśli poniższy blok się nie wykona
if ($cos == $costam)
 {
 session_register(zalogowany, zm2);
 $zalogowany = 1;
 // ...
 }
?>

// plik2.php wywołany z parametrem zalogowany=1
<?php
session_start();
if ($zalogowany == 1) echo 'jesteś zalogowany';
?>
Listing 9. Podszycie się pod zmienną sesyjną za pomocą GET.


Przesyłanie zdjęć na serwer

Z tym zagadnieniem wiążą się dwa problemy. Pierwszy polega na sprawdzeniu czy pliki spełniają kryteria postawione przed aplikacją, a drugi na odpowiednim zabezpieczeniu katalogów w których pliki mają być przechowywane.
W przypadku uploadu plików metodą POST otrzymujemy tablicę $HTTP_POST_FILES w której mamy informacje o nazwie tymczasowej pliku (tmp_name), rozmiarze pliku (size) i oryginalnej nazwie pliku (name). Jest także udostępniana zmienna type zawierająca ustawiony przez przeglądarkę typ mime pliku np. image/gif.
Jeśli dopuszczamy tylko przesyłanie zdjęć warto dodatkowo sprawdzić typ pliku za pomocą funkcji GetImageSize() np. $size = GetImageSize($HTTP_POST_FILES[plik][tmp_name]);
Odpowiednie zabezpieczenie katalogów przeznaczonych na zdjęcia jest bardziej złożone.
      Najlepszym rozwiązaniem, wymagającym niestety własnego serwera lub bardzo dobrych stosunków z administratorem naszego ISP jest stworzenie osobnego konta FTP pozwalającego tylko i wyłącznie operować w katalogu img. W takim przypadku po przesłaniu pliku via formularz WWW na serwer, za pomocą funkcji FTP pobieramy plik z katalogu tymczasowego i umieszczamy w stosownym podkatalogu img. Dzięki czemu katalogom img/mini i img/big możemy nadać prawa dostępu 705.
Minusem tego sposobu jest to że wszelkie operacje na plikach zdjęć musimy wykonywać za pomocą FTP.
      Drugim z ciekawszych rozwiązań jest ustawienie przez administratora serwera odpowiednich grup i praw dostępu do w/w katalogów. Na przykład, serwer WWW chodzi na prawach użytkownika apache, nasz użytkownik to zdzichu. Prosimy więc administratora aby ustawił dla katalogów mini/ i big/ grupę apache, właściciela zdzichu i prawa dostępu 775.
      Trzecim rozwiązaniem, które możemy zrealizować praktycznie na każdym serwerze bez pomocy administratora jest przechowywanie zdjęć w katalogu o bardzo długiej i trudnej nazwie, umieszczonego najlepiej ponad DOCUMENT_ROOT.
Pobieranie zdjęć odbywa się wtedy za pomocą skryptu ustawiającego odpowiednie nagłówki, odczytującego porcjami plik z sekretnego miejsca i wysyłającego te dane do przeglądarki. Niestety takie rozwiązanie obciąża znacznie serwer.
      Istnieją także inne możliwości, np. włączenie trybu bezpiecznego (safe mode) w PHP, niestety mechanizm ten mimo bardzo dobrych założeń nie do końca spełnia swoje zadanie.
Innym rozwiązaniem stosowanym niestety bardzo rzadko (z powodu trudności w odpowiednim skonfigurowaniu) przez ISP jest uruchamianie serwera WWW z prawami właściciela wirtualnego serwera (virtual host), w takim przypadku wystarczą prawa 700.

Przechowywanie zakodowanych haseł

Wszelkie dane poufne takie jak np. hasła administratorów powinny być przechowywane w bazie danych w formie zaszyfrowanej. Można do tego wykorzystać standardową funkcję crypt() lub gdy potrzebujemy lepszego zabezpieczenia użyć funkcji z modułu mcrypt.
Podczas logowania wprowadzone hasło z formularza powinno być zakodowane następnie w skrypcie porównane z hasłem pobranym z bazy danych. Jeśli tylko istnieje taka możliwość to należy stosować bezpieczne protokoły sieciowe takie jak SSL do przesyłania formularza zamówieniowego i logowania.

Weryfikacja danych z formularza

Bardzo istotnym elementem zabezpieczenia się przed złośliwymi "klientami" jest skrupulatna weryfikacja danych pobranych z formularza zamówieniowego. Należy ją wykonywać po stronie serwera, wszelkie sprawdzanie client-side np. za pomocą JavaScript nie mają wielkiego znaczenia, ze względu na łatwość obejścia takich zabezpieczeń. Sprawdzać trzeba każde pole formularza, które jest wymagane do zawarcia transakcji. Kryteria, które zastosujemy powinny być dość surowe np. imię może zawierać tylko litery i musi mieć minimum 3 znaki.
Do tego typu weryfikacji niezbędne są wyrażenia regularne, za pomocą których możemy szybko, łatwo i przyjemnie sprawdzać nawet bardzo złożone kryteria.

Niekiedy warto wprowadzić obowiązek wypełnienia takich unikalnych danych jak NIP lub PESEL, pozwoli nam to zastosować algorytmy do sprawdzania poprawności tych danych i na starcie już odstraszyć wielu potencjalnych kawalarzy.
      Zachęcam do stosowania licznika błędnych wypełnień formularza, zliczających wszystkie kolejne próby wysłania błędnie wypełnionego formularza. Realizacja takiego licznika jest bardzo prosta, po zatwierdzeniu formularza następuje weryfikacja danych jeśli wystąpił jakikolwiek błąd to zwiększamy licznik błędów i zapisujemy do sesji (jeśli wcześniej nie istniał w sesji to wpisujemy 1).
Następnie zwracamy użytkownikowi do poprawki formularz z wpisanymi przez niego danymi oraz zaznaczonymi błędami. Po ponownym wysłaniu formularza procedura się powtarza.
W panelu administracyjnym obok danych zamówienia prezentujemy wskazanie licznika, na podstawie tych danych opiekun sklepu może podjąć decyzję czy warto ryzykować przy realizacji takiego zamówienia.
 
<?php
function check_pesel($pesel)
{
 global $pesel_sex;

 if (strlen($pesel) != 11 || !is_numeric($pesel))
    return 0;
 if (($pesel[9] % 2) == 0)
    $pesel_sex = ' kobieta';
    else $pesel_sex = 'mężczyzna';

  $steps = array(1, 3, 7, 9, 1, 3, 7, 9, 1, 3);
  for ($x = 0; $x < 10; $x++)
      $sum_nb += $steps[$x] * $pesel[$x];
  $sum_m = 10 - $sum_nb % 10;
  if ($sum_m == 10) $sum_m = 0;
  if ($sum_m == $pesel[10]) return 1;
  return 0;
}
?>
Listing 10. Funkcja sprawdzająca poprawność numeru PESEL i zwracająca płeć właściciela.

< część 1 | część 2

sklepy_lst.zip - Wszystkie listingi zawarte w artykule (4 KB).

Artykul został pierwotnie opublikowany w magazynie Software 2.0 3/2002 - "Programowanie dla Internetu". Software 2.0
www.software.com.pl


Copyright (c) 2002-03 by Adam Major (Adam.Major[at]ivpro.net)