wtorek, 4 listopada 2008

uprowadzenie sesji

Dawno nic nie napisałem ciekawego. Ostatnio same zawirowania. Do tego zainstalowałem StarCrafta na Ubuntu oraz znalazłem sposób na kolorowanie kodu w tym oto blogu. Wydaje mi się on teraz nieco bardziej czytelny. Pomału na szczęście wychodzę na prostą, przynajmniej jeśli chodzi o bloga...

Jako młode pachole, prawie każdy z nas chciał być strażakiem, policjantem, pilotem... Ja chciałem być pilotem, a najlepiej astronautą. Nic z tego niestety nie wyszło.

Jako starsze pachole, już nie każdy, ale przynajmniej niektórzy chcieli być hakerami. Łamać hasła, włamywać się do NASA, wykradać materiały o UFO. Wszystkich, którzy wciąż mają takie zamiary, uprzejmie informuję, że ich do tego broń boże nie zachęcam. Jednak jak coś już wykradniesz z NASA, pochwal się w komentarzu.

No ale do rzeczy. Dziś wpis z gatunku bezpieczeństwa. Pomysł nań nasunął mi się podczas rozważań co do mechanizmu logowania w prostej aplikacji WWW. Aplikacja prosta, więc możnaby autoryzację zaimplementować samemu. Można też użyć czegoś gotowego, np. SpringSecurity. Rozwiązanie to raczej pewne i sprawdzone, ale czy nie będzie to strzelanie armatą do muchy?

Rozważmy zatem pierwsze podejście.

Stworzyłem przykładową aplikację, która przechowuje poufne informacje o zarobkach pracowników. Na aplikację składają się 3 klasy:
User, która zawiera informacje o pracowniku

public class User {
private String login;
private String password;
private int balance;

public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}

}

UserManager to klasa, która umożliwia proste operacje na użytkownikach. W prawdziwej aplikacji klasa zapewne będzie ukryta za interfejsem, korzystając do tego z obiektów DAO i inne takie. Na potrzeby jednak naszej aplikacji poniższy menadżer wystarczy:

public class UserManager {

private List<User> users;

public UserManager() {
users = new ArrayList<User>(3);
User user1 = new User();
user1.setLogin("user1");
user1.setPassword("pass1");
user1.setBalance(54321);
User user2 = new User();
user2.setLogin("user2");
user2.setPassword("pass2");
user2.setBalance(65354);
User user3 = new User();
user3.setLogin("user3");
user3.setPassword("pass3");
user3.setBalance(91254);
users.add(user1);
users.add(user2);
users.add(user3);
}

public boolean check(String login, String password) {
User user = loadUser(login);
return user != null && user.getPassword().equals(password);
}

public User loadUser(String login) {
for (User user : users) {
if (user.getLogin().equals(login)) {
return user;
}
}
return null;
}

public void setUsers(List<User> users) {
this.users = users;
}
}

Pozostała jeszcze klasa UserLoginBean, która odpowiada za zalogowanie użytkownika oraz sprawdza czy i jaki użytkownik jest w danym momencie zalogowany.

public class UserLoginBean {
private String login;
private String password;
private User loggedUser;
private UserManager userManager;

public String login() {
if (userManager.check(login, password)) {
loggedUser = userManager.loadUser(login);
login = null;
password = null;
return "success";
}
return "faliture";

}

public String logout() {
userManager = null;
return null;
}

public boolean isUserLogged() {
return loggedUser != null;
}

public User getLoggedUser() {
return loggedUser;
}

public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

Informacje o zalogowanym użytkowniku przechowywane są w sesji, stąd też taki zasięg ziarna UserLoginBean. Widać to w poniższym pliku faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
<managed-bean>
<managed-bean-name>userManager</managed-bean-name>
<managed-bean-class>pl.matt.UserManager</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>userLoginBean</managed-bean-name>
<managed-bean-class>pl.matt.UserLoginBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>userManager</property-name>
<value>#{userManager}</value>
</managed-property>
</managed-bean>
<navigation-rule>
<from-view-id>/pages/login.xhtml</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/pages/home.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>faliture</from-outcome>
<to-view-id>/pages/login.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<application>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config>

do kompletu aplikacji brakuje jeszcze pliku web.xml

<?xml version="1.0"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>sessionHijacktion</display-name>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<context-param>
<param-name>facelets.REFRESH_PERIOD</param-name>
<param-value>2</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
</web-app>

oraz kilku stronek JSF. Dwie pierwsze, nie są specjalnie ciekawe. Strona login.xhtml zawiera prosty formularz, umożliwiający zalogowanie użytkownika do aplikacji

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">
<f:loadBundle basename="resources" var="msg" />
<ui:composition template="/templates/common.xhtml">
<ui:define name="pageTitle">Login form</ui:define>
<ui:define name="pageHeader">Proszę się zalogować</ui:define>
<ui:define name="body">
<h:message showSummary="true" showDetail="false" style="color: red; font-weight: bold;" for="name" />
<h:form id="helloForm">
<h:panelGrid columns="2">
<h:outputText value="login" />
<h:inputText required="true" id="name" value="#{userLoginBean.login}" />
<h:outputText value="password" />
<h:inputSecret required="true" id="password" value="#{userLoginBean.password}" />
<h:outputText value="" />
<h:commandButton id="submit" action="#{userLoginBean.login}" value="zaloguj" />
</h:panelGrid>
</h:form>
</ui:define>
</ui:composition>
</html>

Natomiast strona home.xhtml przedstawia poufne informacje o każdym z użytkowników

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">

<f:loadBundle basename="resources" var="msg" />
<ui:composition template="/templates/common.xhtml">
<ui:param name="loggedOnly" value="true" />
<ui:define name="pageTitle">Greeting to User</ui:define>
<ui:define name="pageHeader">Greeting Page</ui:define>
<ui:define name="body">
#{msg.greeting} #{userLoginBean.loggedUser.login} masz na koncie #{userLoginBean.loggedUser.balance}
</ui:define>
</ui:composition>
</html>

Na uwagę zasługuje parametr

<ui:param name="loggedOnly" value="true" />

dzięki któremu treść strony home.xhtml jest widoczna tylko dla zalogowanych użytkowników.
Jak to działa? Wszystko staje się jasne, gdy obejrzymy plik /templates/common.xhtml zawierający szablon każdej strony aplikacji:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">

<f:loadBundle basename="resources" var="msg" />
<head>
<title><ui:insert name="pageTitle">Page Title</ui:insert></title>
</head>

<body bgcolor="#ffffff">
<table style="border:1px solid #CAD6E0" align="center" cellpadding="0" cellspacing="0" border="0" width="400">
<tbody>

<tr>
<td class="header" height="42" align="center" valign="middle" width="100%" bgcolor="#E4EBEB">
<ui:insert name="pageHeader">Page Header</ui:insert>
</td>
</tr>
<tr>
<td height="1" width="100%" bgcolor="#CAD6E0"></td>
</tr>

<tr>
<td width="100%" colspan="2">
<table width="100%" style="height:150px" align="left" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td align="center" width="100%" valign="middle">
<ui:fragment rendered="#{!loggedOnly || userLoginBean.userLogged}">
<ui:insert name="body">Page Body</ui:insert>
</ui:fragment>
<ui:fragment rendered="#{loggedOnly and !userLoginBean.userLogged}">
Proszę się zalogować
</ui:fragment>
</td>
</tr>
</tbody>
</table>
</td>
</tr>

<tr>
<td colspan="2" valign="bottom" height="1" width="100%" bgcolor="#CAD6E0"></td>
</tr>
</tbody>
</table>
</body>

</html>

a dokładnie jego fragment:

<ui:fragment rendered="#{!loggedOnly || userLoginBean.userLogged}">
<ui:insert name="body">Page Body</ui:insert>
</ui:fragment>
<ui:fragment rendered="#{loggedOnly and !userLoginBean.userLogged}">
Proszę się zalogować
</ui:fragment>

Jeżeli parametr loggedOnly ustawiony jest na wartość true, zawartość strony zostanie wyświetlona, jedynie zalogowanemu użytkownikowi.

Prosta aplikacja, proste zabezpieczenie, ale działają.
Czy na pewno?

Odpalam aplikację na serwerze JBoss 4.2.1.GA.
Wyłączam w przeglądarce obsługę ciasteczek. Próbuję się zalogować, ale nie znam hasła.


mogę się więc jedynie obejść smakiem:


Niby nic. Dla mojego żądania serwer otworzył nową sesję, a jej identyfikator dokleił do adresu URL. Nie ma się co dziwić, w końcu moja przeglądarka nie obsługuję ciasteczek.

Teraz, jako, że nie znam hasła, a chcę się zalogować, kopiuję URL z identyfikatorem sesji i przesyłam mojej ofierze. Oczywiście proszę ją podstępnie o zalogowanie się.


Ofiara moja grzecznie klika w URL i się loguje jako użytkownik "user3".

A ja jeszcze grzeczniej, zmieniam w moim URLu adres z login.jsf na home.jsf (zostawiam identyfikator sesji) i...



Bingo. Też jestem zalogowany!

Jak to się stało? Ano przesyłając adres z dopiętym identyfikatorem sesji, ofiara ataku zalogowała się do aplikacji korzystając mojej sesji. Ja następnie otworzyłem zabezpieczoną stronę, korzystając już z naszej wspólnej sesji, w której właśnie się ktoś zalogował.

Jak się przed takim atakiem bronić? Najlepszym rozwiązaniem jest przydzielanie nowej sesji użytkownikowi przy logowaniu. Tak robi właśnie SpringSecurity. Przy okazji kopiuje on z automatu wszystkie obiekty ze starej sesji, do nowej. Tak więc fakt całej operacji jest dla użytkownika jest niezauważalny.
Więcej o tym ataku znajdziesz na Wikipedii.
Co ciekawe, przesiadka na JBossa 4.2.2.GA uniemożliwia już ten atak, przynajmniej w takiej prostej formie, jak ta opisana powyżej.

Pozostaje tylko się domyślać, przed iloma jeszcze nieznanymi mi typami ataków chroni mnie i moją aplikację SpringSecurity.

8 komentarzy:

WooKasZ pisze...

Witam!
A masz może gdzieś pod ręką tutorial jak zrobić logowanie do aplikacji za pomocą spring security ?

MZ pisze...

tutoriala nie, na pewno znajdziesz przykład w projekcie swf-booking-faces (jest do ściągnięcia razem ze SpringWebFlow) albo w dokumentacji od SpringSecurity.

WooKasZ pisze...

To może pokusiłbyś się o napisanie krótkiego tutoriala ? :>

MZ pisze...

myślę, że da się zrobić

a pisze...

To jeszcze podziel się, jak najłatwiej kolorwać kod na blogspocie? :)

Twóje rozwiązanie kolorowania jest ok, ale brakuje jednego, trzeba wciskać copy, zamiast normalnie zaznaczyć i skopiować, aby otrzymać normalny tekst :)

Racjonalny Developer

Unknown pisze...

Ciekawy artykuł. Warto wspomnieć, że strony JSF pisane są pod dyktando facelets. Osoby pocztkujące mogłyby tego nie zauważyć.

Dodatkowo usuń te powtarzające się </user>, które kończą typy parametryzowane (List<user>). Dodatkowo user pisany jest z małej litery w deklaracji listy.

MZ pisze...

dzięki. Dodatkowe tagi <user>; generował dodatek kolorujący składnię kodu java, ponieważ nie zamieniłem < ani > na odpowiednio &lt; i & Teraz jest już lepiej.

MZ pisze...

Did you understand anything in polish?