Dziś jest czwartek, 17 maja 2012 roku (z kalendarza...)

Poeksperymentujmy z kontrolą uprawnień

Icon

23.09.2010, 20:55

PHP

Komentarze (5)

Powrót

W dzisiejszym wpisie zajmiemy się tematyką kontroli uprawnień w aplikacjach, ale od nieco innej strony. Podczas gdy inne artykuły przedstawiają konkretne techniki realizacji list uprawnień i algorytmy ich sprawdzania, ja zajmę się zagadnieniem bagatelizowanym, a nawet wręcz nieistniejącym w świadomości programistów. Spróbuję przedstawić ideę działania oraz sposób implementacji ogólnego mechanizmu kontroli uprawnień, całkowicie oderwanego od takich konkretnych technik i zdolnego obsłużyć każdą z nich.

Motywacja

Na początku wyjaśnię może, skąd w ogóle przyszedł mi do głowy tak idiotyczny na pierwszy rzut oka pomysł. Jeśli tworzyliśmy je w Zend Frameworku, do naszej dyspozycji został oddany potężny i całkiem rozbudowany komponent Zend_Acl. Inne frameworki także oferują podobne mechanizmy. Ale czy zawsze potrzebujemy tak zaawansowanych scyzoryków szwajcarskich? W ramach mojego projektu inżynierskiego realizuję w PHP panel internetowy systemu automatycznego oceniania i testowania programów studentów dla mojej uczelni. Projekt jest dość duży, ale jednak cała kontrola uprawnień sprowadza się tam de facto do rozróżniania poszczególnych typów użytkowników i wpuszczania ich do jednej z trzech części panelu. Po prostu więcej nie potrzeba - jako system wewnętrzny, w którym cała administracja zna się nawzajem, jakiekolwiek większe udziwnienie wprowadziłoby po prostu niepotrzebny zamęt. Tak, projektów z minimalistyczną kontrolą uprawnień jest całkiem sporo i tu stajemy przed dylematem: jeśli odrzucimy domyślny mechanizm, zostajemy bez systemu ACL i albo zabawimy się sklecenie jakiejś prowizorki, albo napiszemy własny od zera. Dlaczego zatem nie uderzyć głębiej i nie zaprojektować czegoś bardziej niskopoziomowego, co będzie prosto skalowalne na dowolny właściwy mechanizm ACL?

Założenia

Zaczynamy od zdefiniowania założeń naszego przedsięwzięcia. Chcemy zbudować system ACL, który:

  • umożliwi sprawdzenie dostępu do określonego miejsca na zasadzie przyznany lub nieprzyznany,
  • będzie całkowicie niezależny od sposobu składowania, zależności itd. między miejscami objętymi kontrolą dostępu,
  • będzie całkowicie niezależny od zasad przyznawania dostępu do konkretnego miejsca,
  • umożliwi zabezpieczenie aplikacji przed samą sobą (np. z powodu obecności modułów stworzonych przez zewnętrznych dostawców lub pochodzącego z nieoficjalnych źródeł).

Jest ciężko, bowiem jak można zaprojektować sprawdzanie dostępu, jeśli domyślnie nie możemy wiedzieć absolutnie nic? A zabezpieczanie skryptu przed samym sobą? Większość aplikacji z tego raczej nie będzie korzystać, ale ogólnodostępne CMS-y open-source przyjęłyby coś takiego z pocałowaniem ręki. Ostatecznie chcielibyśmy mieć pewność, że ściągnięty właśnie moduł nie wczyta sobie z konfiguracji hasła naszej bazy danych i nie wyśle go do autora... Ponadto to jest też eksperyment myślowy, a czym by on był bez wyzwań?

Opis teoretyczny

Prace zaczynamy od zdefiniowania sobie kilku podstawowych terminów, którymi będziemy operować. Pojęciem pierwotnym będzie dla nas zdarzenie. Nie interesuje nas, czym ono jest - istotne jest, że może posiadać jeden z dwóch stanów:

  • może być wykonane
  • nie może być wykonane

Przekładając to na zrozumiały język, zdarzenie będzie jakąś czynnością, którą chcemy wykonać i co do której chcemy mieć pewność, że możemy ją wykonać. Nasz system ACL będzie odpowiadał na to pytanie, zwracając zgodę lub odmowę.

O tym, czy dostęp zostanie faktycznie przyznany, zdecyduje polityka bezpieczeństwa. To właśnie ona odpowiada za spełnienie drugiego i trzeciego założenia, ponieważ to nie my będziemy definiować politykę - zrobi to programista piszący konkretną aplikację, który zna swoje potrzeby. Cała idea polega właśnie na tym, by nasz system ACL udostępniał jednolite API i w tle przekazywał żądanie do konkretnej polityki, którą może już realizować np. Zend_Acl.

Ale to nie wszystko, bowiem wciąż pozostało nam do spełnienia ostatnie założenie i tu zaczyna się prawdziwa zabawa. Musimy bowiem wymyślić mechanizm (i to w PHP!), który:

  1. nie odetnie nas od podstawowych funkcji,
  2. dopuści zewnętrzny kod tylko do operacji, na które mu zezwolimy,
  3. nie pozwoli zewnętrznemu kodowi zmienić żadnych ustawień bezpieczeństwa,
  4. pozwoli zmieniać naszemu kodowi ustawnienia bezpieczeństwa.

Dlatego zdefiniujemy sobie kolejną rzecz, mianowicie domenę. Domena jest pewnym obszarem, w którym obowiązuje pewna polityka bezpieczeństwa. Obszar rozciąga się na ściśle określony kawałek kodu tak i nigdzie indziej. Jeśli polityka obszaru X zezwala na zajście zdarzenia Y, to kod nienależący do obszaru nie może z tego zezwolenia skorzystać. Wbrew pozorom, te na pierwszy rzut oka absurdalne ograniczenia bardzo łatwo zrealizować przy pomocy języka obiektów. Każda domena jest obiektem, a kod należący do konkretnej domeny rozpoznajemy po tym, że posiada on dostęp do obiektu domeny. Za granice obszaru robi nam tu widzialność zmiennych PHP. Jeśli chcemy zewnętrznemu kodowi odciąć dostęp do niektórych funkcji, po prostu przekazujemy mu oddzielną domenę z jakąś paranoiczną polityką i upewniamy się, że nie ma on skąd pobrać domeny, w której pracuje reszta aplikacji.

To jednak nie wszystko. Gdyby świat kończył się tylko na obiekcie domeny, całość byłaby łatwa do oszukania - tworzymy nowy obiekt domeny, wrzucamy mu luźną politykę i z powrotem możemy czynić swoje. Co to to nie. Potrzebujemy dodatkowego elementu, czyli menedżera polityk. Będzie on sprawował pieczę nad dostępnymi politykami oraz domenami. Tylko on może tworzyć obiekty tych dwóch rodzajów, konfigurować je, a dla pewności udostępnia operację verifyDomain(), która pozwala sprawdzić czy dana domena została faktycznie utworzona przy pomocy tego menedżera obiektów. Jeśli całość pozabezpieczamy poprzez zastrzeżenie kluczowych pól jako private, a metod jako final, po wstępnej konfiguracji możemy być już bezpieczni o nasz kod.

Polityki w praktyce

Cały powyższy opis może wydawać się dość abstrakcyjny. Pierwsze pytanie, jakie się nasuwa, to jak do licha zabronilibyśmy zewnętrznemu kodowi wykonać jakąś czynność? Cóż, faktycznie nie da się zablokować np. możliwości użycia pętli foreach :) ale przecież nasz system posiada często rozbudowane API. Sęk w tym, by poukrywać sprawdzanie dostępu właśnie w tym API i wymagać jako argument domeny wywołującego:

public function safeOperation($argument, Domain $domain)
{
	if(!$this->_policyManager->verifyDomain($domain))
	{
		throw new Exception('Arghhhh! Hostile domain in action!');
	}
	if($domain->isAllowed(new Event('safeOperation')))
	{
		// robimy swoje
	}
} // end safeOperation();

Teraz sprawa powinna być prosta: nasz kod przekaże luźną domenę, która zezwoli na wykonanie tej operacji. Cudzy kod z domeną specjalnej troski zostanie zatrzymany na operacji isAllowed(), a gdyby próbował wprowadzić do systemu fikcyjną domenę, zaprotestuje menedżer polityk. W ten sposób możemy zabezpieczyć np. odczyt kluczowych informacji z konfiguracji takich, jak hasła bazy, albo odczyt haseł użytkownika.

Ciekawie wygląda zabezpieczenie samego menedżera polityk przed grzebaniem w nim. Oczywiście musimy udostępnić takie operacje, jak addPolicy(), createDomain(), setDomainPolicy() czy getDomain(), ale oczywiste jest, że nie każdy powinien mieć możliwość ich wywoływania. Z drugiej strony, musimy pozostawić sobie też możliwość skonfigurowania świeżo utworzonego obiektu. Da się to zrealizować bardzo prosto: niech menedżer polityk ma też swoją własną domenę, a ponadto udostępnia operacje lock() i unlock(). Zablokowanie działa podobnie, jak blokada telefonu, tj. menedżer przechodzi w tryb tylko do odczytu i jedyne, co możemy zrobić, to go ewentualnie odblokować. Sęk w tym, że bezpieczna operacja unlock() wygląda tak:

final public function unlock(Domain $currentDomain)
{
	if($this->_internalDomain !== null)
	{
		if(!$this->verifyDomain($currentDomain))
		{
			throw new Exception('Arghhhh! Hostile domain in action!');
		}
		$this->_internalDomain->verifyScream(new Event('policyManager.unlock', array('domain' => $currentDomain)));
	}
 
	$this->_locked = false;
} // end unlock();

OK, widzimy, że aby odblokować menedżer polityk, musimy należeć do jakiejś domeny i tę domenę musimy do odblokowywarki przekazać. Tam wykona się standardowa procedura uwierzytelniania domeny, po czym nastąpi odwołanie do wewnętrznej domeny z pytaniem: "czy domena X może odblokować menedżera polityk?" Analogicznie wygląda sprawa z metodą getDomain(), gdzie musimy zadać pytanie: czy kod X działający w domenie Y może uzyskać obiekt domeny Z?

Nieco inny trick został zastosowany do ustawiania domenie polityki bezpieczeństwa. Oczywiste jest, że aby obiekt klasy PolicyManager mógł wywołać metodę Domain::setPolicy(), metoda ta musi być publiczna, a to otwiera pole do potencjalnych nadużyć... o ile nie użyjemy tokena. Sztuczka polega na tym, by obiekt PolicyManager w momencie tworzenia generował sobie jakiś skomplikowany, tekstowy klucz przechowywany w prywatnym polu i niedostępny w ogóle z zewnątrz. Ponieważ za utworzenie domeny odpowiada menedżer, może przekazać do niej ten klucz jako argument konstruktora. W domenie także klucz będzie trzymany w prywatnym polu, bez dostępu z zewnątrz. Aby wywołać setPolicy(), należy podać prawidłowy klucz, a ponieważ zna go tylko menedżer, tylko on może zmienić politykę.

Konsekwencje użycia

Pora na analizę mocnych i słabych stron zaproponowanego rozwiązania. Zacznijmy od zalet:

  • API systemu ACL jest niezależne od używanej polityki, dzięki czemu możemy bezpiecznie używać wymuszania typów w odniesieniu do niego.
  • Zmiany w polityce są odseparowane od sposobu korzystania z niej, dzięki czemu unikamy problemów związanych z kompatybilnością reszty aplikacji podczas jej rozwijania.
  • Możemy transparentnie zmienić politykę bezpieczeństwa na inną.
  • Mamy możliwość zabezpieczenia kluczowych funkcjonalności przed działaniem kodu osób trzecich, co jest niezwykle istotne w wybranych rodzajach projektów.

Wady:

  • Mechanizm samoograniczania dostępu często nie jest potrzebny.
  • Niektóre polityki mogą udostępniać bardziej złożone wyniki kontroli uprawnień. Konieczne jest ich tłumaczenie na język binarnych zdarzeń.

Przykładowa implementacja

Aby nie być gołosłownym, tradycyjnie stworzyłem eksperymentalną implementację będącą zarazem startem kolejnego projektu z serii OPL o nazwie Open Power Security. Z pomysłem stworzenia frameworka bezpieczeństwa nosiłem się już od jakiegoś czasu, dostrzegając zupełny brak rozwiązań tego typu, ale stale przeszkadzał mi brak czasu. Dopiero niedawne eksperymenty z Trinity zmusiły mnie do napisania choćby prostego szkieletu biblioteki (podobnie jak rozwinięcia legendarnego OPF-a). Mam nadzieję, że idea budowy czegoś takiego Wam się spodoba i zarówno pomysł przedstawiony powyżej, jak i sama biblioteka będą warte dalszego rozwoju. Z kodem można zapoznać się na Githubie.

Zakończenie

W wielu aplikacjach kwestia bezpieczeństwa schodzi często na dalszy plan i jest traktowana jako dodatek. Nic dziwnego - jak sam stwierdziłem na początku, często nie potrzeba nam żadnych cudów. Niemniej nie znaczy to, że zagadnienie można całkowicie zbagatelizować - warto mieć pod ręką zestaw, który sprawdzi się w każdej aplikacji, zarówno małej, jak i dużej. Tak w ogóle to parę koncepcji dotyczących powyższego pomysłu "pożyczyłem" z nakładki na jądro Linuksa o wdzięcznej nazwie SELinux, która podnosi kontrolę uprawnień w tym systemie na niespotykany poziom abstrakcji. Miałem okazję się nią niedawno pobawić i choć na domowym komputerze w życiu jej pewnie nie zainstaluję, to nie da się ukryć, że jest to kawałek niezłej roboty.

PS. Powyższy sposób działa przy założeniu, że nie da się odczytać i modyfikować prywatnych pól obiektu z zewnątrz. Niestety, począwszy od PHP 5.3 jest już inaczej.

Powrót

Komentarze

avatar

Napisał sokzzuka w czwartek, 23 września 2010 o 23:17

Też kiedyś eksperymentowałem z kwestią dostępu i bezpieczeństwa. Rozwiązanie jakie stworzyłem polegało na tym, że wszystkie obiekty w aplikacji zarządzane były przez kontener IOC. W momencie gdy chcieliśmy uzyskać dostęp do jakiegoś obiektu, który potrzebował ograniczeń dostępu, zwracane było zamiast niego Proxy, przez które leciały wszystkie wywołania metod do tego obiektu i przy każdym wywołaniu dokonywano sprawdzania uprawnień w ACL i ewentualnie zwracano wyjątek.

avatar

Napisał deallas w piątek, 24 września 2010 o 01:46

Ochrona aplikacji przed samą sobą ? Zadanie niemal niemożliwe w PHP... Nadal istnieje dostęp do plików, które bez problemu można odczytać. To samo tyczy się zmiennych 'private'. Wystarczy prosty var_dump na obiekcie i wszystko mamy jak na dłoni. Być może nie zrozumiałem do końca o co ci chodziło...
Ja bym to zrobił nieco inaczej. Menedżer uprawnień uruchomiłbym jako usługę i ustawił na oddzielnym użytkowniku tak, aby nikt nie miał dostępu do chronionych plików/zasobów. Domeny (klienci) wysyłałyby tylko określone polecenia do usługi i dostawałyby konkretną odpowiedź.
Nie wyobrażam sobie inaczej, żeby bezpiecznie to funkcjonowało...

avatar

Napisał Zyx w piątek, 24 września 2010 o 08:17

Hmmm... var_dump() można zablokować jako niedozwoloną funkcję, ale faktycznie - alternatywnym sposobem zabezpieczenia jest wykorzystanie dowolnego obiektu w roli klucza. Wtedy nawet var_dump() nic nie da, gdyż dostaniemy jedynie identyfikator danego obiektu, a nie sam obiekt.

Nawiasem mówiąc w PHP 5.3 pojawił się jeszcze jeden sposób odczytu, a także... modyfikacji prywatnych właściwości obiektu.

avatar

Napisał Gryf w piątek, 24 września 2010 o 08:26

"dostęp do plików" tak tylko od tego masz encoder'y
a dostęp do funkcji zawsze możesz ograniczyć -> runkit.

avatar

Napisał deallas w wtorek, 5 października 2010 o 00:21

Do Ioncube i Zend Guard istnieją dekodery więc to żadne zabezpieczenie...
Co do runkit'a to byłoby to dobre rozwiązanie o ile działałoby z najnowszą wersją PHP. Starszej wersji 0.9 nie da się skompilować, zaś nowsza 1.0dev nie oferuje wszystkich możliwości i w dodatku jest niestabilna. Poza tym ciekaw jestem czy pod runkit'em działałby APC lub inny akcelerator...

Pamiętaj, dbaj o kulturę wypowiedzi oraz dyskusji w sieci.

Skomentuj

NickInformacja
E-mailNa wypadek potrzeby kontaktu z autorem (niepublikowany)
BlogNie zapomnij o http://
LayoutNapisz tu, czy widzisz dzienny czy nocny layout.
WpisFormatowanie wikiKomentarze są moderowane - przeczytaj zasady!

Na Zyxist.com panuje swoboda wyrażania opinii oraz krytyki pod dowolnym adresem. Jedyny warunek: musi być ona kulturalna i rzeczowa. Na chamstwo, prostactwo lub jawne obrażanie kogokolwiek nie ma tu miejsca i takie komentarze są bardzo szybko usuwane. Jeśli zamierzasz polemizować z treścią wpisu, wpierw uważnie ją przeczytaj.

© Tomasz "Zyx" Jędrzejewski 2005 - 2012 | Wykonanych zapytań: 2 | Serwer wirtualny zapewnia