Cookies und Sessionhandling

HTTP ist ein zustandsloses Protokoll, welches den Begriff einer Session nicht kennt. Es verwaltet keine Daten wie etwa eine eindeutige Identifikationsnummer eines Clients. Der Begriff Session wird erst in der Anwendungsschicht eingeführt. Kann ein Hacker eine Session eines Users übernehmen spricht man von Session Hijacking. Das folgende Kapitel befasst sich mit Methoden um die Session eines Users zu schützen.

Tracking Mode

In Java EE kann man konfigurieren wie eine Session identifiziert werden soll. Dabei gibt es 3 Möglichkeiten (SessionTrackingMode):

  • URL: Dabei wir eine Identifikationsid (JSESSIONID) als URL Parameter verwendet.
  • COOKIE: Die Sessionid wird in einem Cookie abgelegt, welches bei jedem Request wieder mitgeschickt wird.
  • SSL: Die SSL Sessionid wird als Identifikation für die Session verwendet.

Laut Spezifikation kann man Url und Cookie gemeinsam benutzen nur SSL darf nur für sich selbst stehen.

Konfigurieren kann man den TrackingMode unter anderem (andere Möglichkeite werden später im Kapitel erläutert) im web.xml.

1
2
3
<session-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>

Trackingmode URL

Url ist dabei wohl die unsicherste Variante wie man die Sessionid übertragen kann. Die Sessionid findet sich in der Browserhistorie wieder. Teilweise findet man eine Sessionid auch in Suchergebnissen von Suchmaschinen.
In JSF übernimmt das Form-Tag das Anhängen der Sessionid an die Action-URL.

1
2
3
<form id="j_idt5" name="j_idt5" method="post"
action="/security-web/util/information.xhtml;jsessionid=ff0cbc19baea9053ff9863f1a91b"
enctype="application/x-www-form-urlencoded">

Jedoch spielen auch Cookies in der Sicherheitsbetrachtung einer Web-Applikation eine nicht unerhebliche Rolle. Denn es ist auch möglich eine über ein Cookie übertragene Sessionid abzufragen. Zum Beispiel über Javascript. Oder wenn das Cookie über einen HTTP Aufruf ausgeliefert wird und nicht über einen HTTPS. Cookies können jedoch so konfiguriert werden, dass sie vor Angreifern wesentlich besser geschützt sind. Im web.xml kann dazu die cookie-config angepasst werden.

1
2
3
4
5
6
<session-config>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
</session-config>
  • Http-Only bedeutet in diesem Fall, dass der Browser ein Session Cookie zwar über HTTP und HTTPS verschicken darf, jedoch darf das Cookie nicht über Javascript ausgelesen werden.
  • Secure bedeutet, dass der Browser ein Cookie nur über HTTPS verschicken darf.

Diese zwei Einstellungen erhöhen die Sicherheit einer Web-Applikation schon wesentlich. Allerdings, wenn diese Einstellungen im web.xml gemacht werden, wie gehen wir dann mit Entwicklungsrechnern um, auf welchen eventuell gar kein SSL vorhanden ist?

Dazu gibt es einen Trick, man kann das SessionCookie auch im Code konfigurieren, allerdings nur solange der SessionContext nicht vollständig initialisiert ist. Man kann also die CookieConfig nur in einem WebListener setzten.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebListener
public class SecurityServletContextListener
implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent event) {
}
@Override
public void contextInitialized(ServletContextEvent event) {
configureSessionCookie(event.getServletContext());
}
private void configureSessionCookie(ServletContext servletContext) {
SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
ProjectStage projectStage = FacesContext.getCurrentInstance().getApplication().getProjectStage();
if (projectStage == ProjectStage.Development) {
sessionCookieConfig.setSecure(false);
servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));
}
}
}

Die Einstellungen für die Produktion sollten im web.xml stehen, die Ausnahmen für Development, UnitTest und SystemTest können im Code überschrieben werden.

Der WebListener wird entweder mit der Annotation @WebListener registriert, oder wiederum im web.xml eingetragen:

1
2
3
4
5
6
7
<web-app>
<listener>
<listener-class>
at.freelenzer.ui.jsf.util.SecurityServletContextListener
</listener-class>
</listener>
</web-app>

Das gleiche Dilemma wie mit der Cookie Config hat man allerdings auch mit der javax.faces.PROJECT_STAGE. Denn diese wird auch in der web.xml konfiguriert.

1
2
3
4
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>

Darüberhinaus sieht die Spezifikation auch vor, dass man die ProjectStage über JNDI (Java Naming Directory) definieren kann. Dazu sind folgende Schritte notwendig.

1. JNDI Referenz im web.xml eintragen:

1
2
3
4
5
<resource-ref>
<res-ref-name>jsf/ProjectStage</res-ref-name>
<res-type>java.lang.String</res-type>
<mapped-name>javax.faces.PROJECT_STAGE</mapped-name>
</resource-ref>

2. JNDI Resource am Server konfigurieren:

Hier am Beispiel von GlassFish:

jndiProjectStageConfig

  • Factory Class: com.sun.faces.application.ProjectStageJndiFactory
  • JNDI Name: javax.faces.PROJECT_STAGE
  • Type: java.lang.String
  • Property
  • stage: Development

Hier gilt dieselbe Empfehlung wie auch schon vorhin beim den Cookie Einstellungen. Die Einstellungen welche in der Produktion verwendet werden sollen, werden im web.xml eingetragen. Sollte auf einem Enwicklungsarbeitsplatz eine andere Konfiguration benötigt werden, so wird diese auf dem Server konfiguriert und nicht in der Applikation.

Trackingmode SSL

Der Secure Socket Layer, welcher im HTTPS Protokoll verwendet wird, ermöglicht es Requests welche von einem Client kommen, eindeutig zu identifizieren. Der Servlet Container kann diese Information nutzen um eine HTTP Session zu verwalten.
Sollte man mal programmatisch auf die SSL Session ID zugreifen, so kann man dies folgendermaßen machen.

1
String sslID = (String)request.getAttribute("javax.servlet.request.ssl_session_id");

Verfikation des Trackingmodes

Wenn man sich ganz sicher sein will, dass die Sessionid z.B: über ein Cookie kommt und zwar nur über ein Cookie, dann kann man das über einen Servlet Filter einfach lösen. Da wir später noch andere Aspekte über einen Servlet Filter lösen möchten implementieren wir einen Filter der es uns ermöglicht Seurity Aspekte einzuhängen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@WebFilter(filterName = "SecurityFilter", urlPatterns = {"/*"})
public class SecurityFilter implements Filter {
private static final List<SecurityAspect> securityAspects = new ArrayList<>();
static {
securityAspects.add(new SessionTrackingModeAspect());
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
for (SecurityAspect securityAspect : securityAspects) {
securityAspect.doFilter(request, response);
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
for (SecurityAspect securityAspect : securityAspects) {
securityAspect.init(filterConfig);
}
}
@Override
public void destroy() {
for (SecurityAspect securityAspect : securityAspects) {
securityAspect.destroy();
}
}
}

Den ersten Security Aspect (SessionTrackingModeAspect) haben wir schon eingetragen. Dieser könnte in etwa folgendermaßen aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SessionTrackingModeAspect implements SecurityAspect {
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (((HttpServletRequest) request).isRequestedSessionIdFromURL()) {
//Könnte ein Angreifer sein.
System.err.println("Angriff");
((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//don't need
}
@Override
public void destroy() {
//don't need
}
}

Kommt die Sessionid also aus der URL, dann kann man von einem Angreifer oder von einer Fehlkonfiguration der eigenen Anwendung ausgehen.

Session Timeout

Nicht nur das Erzeugen und Tracking einer Session ist wichtig, sondern auch das Beenden der Session. Zum Einen kann man den User die Session invalidieren lassen. Das ist relativ einfach:

1
2
3
4
5
public void invalidateSession() {
FacesContext fCtx = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession) fCtx.getExternalContext().getSession(false);
session.invalidate();
}

Zum Andern sollte man auch die Möglichkeit in betracht, ziehen die Session nach einer bestimmten Zeit zu beenden.
Dabei betrachten wir das inaktivitäts Timeout und ein absolutes Session Timeout.

Inaktivitäts Timeout

Sollte der User vergessen sich auszuloggen, muss die Session vom Server irgendwann aufgeräumt werden. Dazu kann man im web.xml den Session Timeout Parameter setzen.

1
2
3
<session-config>
<session-timeout>30</session-timeout>
</session-config>

Dies bedeutet, wenn der Benutzer 30 Minuten keinen Request gegen die Anwendung macht, wir die Session beendet.

Hat ein Angreifer jedoch schon geschafft die Session eines Users zu übernehmen, ist ein einfaches Session Timeout nicht mehr ausreichend. Der Angreifer könnte durch ein simples Polling die Session unendlich lange aufrecht erhalten.

Abhilfe schafft hier ein absolutes Session Timeout.

Absolutes Session Timeout

Ein absolutes Session Timeout begrenzt die Zeit, die eine Session aufrecht gehalten wird.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AbsoluteSessionTimeoutAspect implements SecurityAspect {
public static final String ABSOLUTE_SESSION_TIMEOUT = "absolute-session-timeout"; //public für die Konfiguration!
private long absoluteSessionTimeout;
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpSession session = req.getSession( false );
if ( session != null) {
long activated = session.getCreationTime();
if ( System.currentTimeMillis() > ( activated + absoluteSessionTimeout * 60 * 1000 ) ) {
session.invalidate();
}
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if ( filterConfig.getInitParameter(ABSOLUTE_SESSION_TIMEOUT) == null ) {
throw new IllegalStateException( "Absolutes Session Timout muss konfiguriert werden: " + ABSOLUTE_SESSION_TIMEOUT );
}
absoluteSessionTimeout = new Long( filterConfig.getInitParameter(ABSOLUTE_SESSION_TIMEOUT) );
}
@Override
public void destroy() {
//don't need
}
}

Die Konfiguration des Aspekts erfogt über einen Initparameter auf dem dazugehörigen Filter. Je nachdem wie der Filter konfiguriert wurde, kann der Initparameter entweder über eine Annotation am Filter oder über die web.xml gesetzt werden.

Annotation:

1
2
3
4
5
6
7
8
@WebFilter(
filterName = "SecurityFilter",
urlPatterns = {"/*"},
initParams = {
@WebInitParam(name = AbsoluteSessionTimeoutAspect.ABSOLUTE_SESSION_TIMEOUT, value = "60") //Eine Stunde
}
)
public class SecurityFilter implements Filter {

web.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<filter>
<description>Der Security Filter</description>
<filter-name>SecurityFilter</filter-name>
<filter-class>at.freelenzer.ui.jsf.util.SecurityFilter</filter-class>
<init-param>
<description>Eine Stunde</description>
<param-name>absolute-session-timeout</param-name>
<param-value>60</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SecurityFilter</filter-name>
<url-pattern>*</url-pattern> TODO
</filter-mapping>

WebListener

Mit der Servlet Spezifikation 3.0 gibt es ein sehr interessantes neues Feature die WebListener. Wir haben es schon weiter oben für den SecurityServletContextListener benutzt.
Die Annotation @WebListener wirkt immer im Zusammenhang mit dem Interface welches die Klasse implementiert (ServletContextListener, ServletContextAttributeListener, ServletRequestListener, ServletRequestAttributeListener, HttpSessionListener, HttpSessionAttributeListener oder HttpSessionIdListener). Wir können den WebListener nutzen um die Erzeugung und die Beendigung von Sessions zu beobachten.

1
2
3
4
5
6
7
8
9
10
11
@WebListener
public class SecurityHttpSeesionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent event) {
System.out.printf("Session ID %s erzeugt um %s%n", event.getSession().getId(), new Date());
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
System.out.printf("Session ID %s zerstört um %s%n", event.getSession().getId(), new Date());
}
}