Response Headers

Jede Nachricht, die an einen Webserver geschickt wird bzw. von einem Webserver empfangen wird, besteht aus 2 Teilen:

  • dem Nachrichtenkopf (Header)
  • dem Nachrichtenrumpf (Body)

Der Nachrichtenkopf beschreibt einerseits den Nachrichtenrumpf genauer. Dabei geht es um Dinge wie die gewünschte Sprache oder den Zeichensatz mit welchem die Nachricht codiert wurde.
Andererseits werden aber gerade bei Antworts Nachrichten (responses), Direktiven an den jeweiligen Browser übergeben, wie zum Beispiel Cookies, welche zu setzen sind oder die Gültigkeitsdauer dieser Nachricht.

Wir kennen bereits ein Response Header-Attribut, das Set-Cookie Attribute. Wir haben es nicht direkt im Code gesetzt, denn das Session Cookie aus dem vorherigen Kapitel wird vom Applikations Server automatisch verwaltet.

1
Set-Cookie: JSESSIONID=6eba983f1b938302c8612269667f; Path=/security-web; HttpOnly

Wir können allerdings die Response Header-Attribute auch im Code manuell setzten. Hier ein kleines Beispiel:

1
2
3
4
HttpServletResponse resp = (HttpServletResponse) response;
Cookie username = new Cookie("username", "John Silver");
//username.setMaxAge(60*60*10);
resp.addCookie(username);

Header:

1
Set-Cookie: username="John Silver"

Dieser Code könnte in einem WebFilter verwendet werden.

In weiterer Folge sehen wir uns weitere Response Header Attribute an, welche Security relevant sind. Leider werden jedoch noch nicht alle Directiven auch von allen Browsern unterstützt, dies sollte man in sein Security Konzept mit einbeziehen. Wenn möglich wird auf die Browser Unterstützung hingewiesen.

X-Frame-Options

Browser Unterstützung

Die X-Frame-Options werden benutzt um anzugeben, ob die angeforderte Seite in einem Frame angezeigt werden darf oder eben nicht, dadurch schützt diese Option unter anderem vor Clickjacking.

Clickjacking ist eine Technik, bei der die Darstellung einer Internetseite überlagert wird. Dadurch werden Nutzer veranlasst, scheinbar harmlose Mausklicks und/oder Tastatureingaben durchzuführen.

Es gibt drei mögliche Werte für X-Frame-Optionen:

  • DENY: Die Seite kann nicht in einem Frame angezeigt werden
  • SAMEORIGIN: Die Seite kann nur in einem Frame angezeigt werden die von der selben Herkunft ist wie die Seite selbst.
  • ALLOW-FROM uri: Die Seite kann nur in einem Frame einer Seite spezifizierten Ursprungs angezeigt werden.

Umsetzung als Security Aspect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class XFrameOptionsAspect implements SecurityAspect {
private String deny = "DENY";
private String sameorigin = "SAMEORIGIN";
private String allowfrom = "ALLOW-FROM http://www.freelenzer.at";
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setHeader("X-Frame-Options", deny);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}

Header:

1
X-Frame-Options: DENY

Versucht man nun diese Seite trotzdem in einem Frame zu öffnen, bekommt man eine Fehlermeldung:

1
2
Refused to display 'http://localhost:8080/security-web/index.xhtml'
in a frame because it set 'X-Frame-Options' to 'DENY'.

HTTP Strict Transport Security

Browser Untestützung:

  • Internet Explorer: IE11
  • Firefox: 4
  • Opera: 12
  • Safari: Mavericks (Mac OS X 10.9)
  • Chrome: 4.0.211.0

HSTS wie HTTP Strict Transport Security abgekürzt wird, wurde erst 2012 unter RFC 6797 veröffentlicht.
Die Idee ist ganz einfach. Wenn ein User eine Website über den Browser aufruft, und dabei per HTTP zugreift, könnte sich in diesem Fall ein Hacker schon als “Man-in-the-Middle” dazwischen hängen. Wurde allerdings schon vorher einmal diese Seite aufgerufen und dabei kam der Response-Header “Strict-Transport-Security” zurück, so wandelt der Browser alle Anfragen an diese Adresse direkt in eine HTTPS Anfrage um.

Beim Header wird der Parameter “max-age” gesetzt. Dieser gibt die Gültigkeitsdauer dieser Directive in Sekunden an. Zusätzlich kann noch über “includeSubdomains” bestimmt werden, dass diese Directive auch für alle Subdomains gelten soll.

Achtung: Chrome verwaltet die sogenannte HSTS preload list, welche unter anderm von Firefox und Safari benutzt wird. Auch IE11 und Edge benutzen eine preload list welche die von Chrome inkludiert.

Umsetzung als Security Aspect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HTTPStrictTransportSecurityAspect implements SecurityAspect {
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
boolean secure = request.isSecure() | "https".equalsIgnoreCase(request.getHeader("X-Forwarded-Proto"));
if (secure) {
response.setHeader("Strict-Transport-Security", "max-age=31536000;includeSubdomains");
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}

Header:

1
Strict-Transport-Security: max-age=216000;includeSubdomains

XSS-Protection

Urprünglich wurde die XSS-Protection Directive von Mircrosoft 2010 mit dem Internet Explorer 8 eingeführt. Mitlerweile unterstützen fast alle gängigen Browser dieses Feature.

Die Syntax für diesen Header schaut folgendermaßen aus:

1
X-XSS-Protection: (0|1{;mode=block}{;report=.*})

Man kann mit diesem Header die vom Browser zur Verfügung gestellte CrossSiteScripting Erkennung steuern. Mit 0 kann man dies für die eigene Seite deaktivieren. Mit 1 kann man sie aktivieren, falls diese der User generell deaktiviert hat.

  • 0 deaktiviert
  • 1 aktiviert, der Browser versucht den XSS Code mittels eingefügten # Zeichen unschädlich zu machen.
  • 1; mode=block aktiviert, allerdings wird nun nach einem erkannten Angriff die Seite nicht mehr gerendert.
  • report=(JSON POST URI) URI an welche ein XSS Alert geschickt werden soll.

Leider kann man in die Browser Engines nicht hineinschauen, und darum kann man auch nur schwer sagen, welche XSS Angriffe wirklich erkennt werden oder nicht.

Hier findet man dazu noch mehr Informationen und vor allem eine Testseite um den Response-Header zu testen.

X-Content-Type-Options

Dazu zeige ich am besten gleich ein Beispiel:

1
2
3
4
5
6
7
8
Content-Type: text/plain
X-Content-Type-Options: nosniff
<html>
<body bgcolor="red">
I am a HTML source Code!
</body>
</html>

Umsetzung als Security Aspect:

1
2
3
4
5
6
7
8
9
10
11
12
public class XContentTypeOptionsAsspect implements SecurityAspect {
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setHeader("X-Content-Type-Options", "nosniff");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}

Würde man hier auf nosniff verzichten, dann würden einige Browser diesen Response als HTML Seite rendern. Die eigentliche Intention war allerdings den Response als plain Text auszuliefern.

X-Download-Options

Dieses Request-Header Attribut erlaubt es den “open” Button beim Download von Dateien zu steuern. Mit noopen wird der Öffnen Button nicht angezeigt.

1
2
3
Content-Type: text/html
Content-Disposition: attachment; filename=untrustedfile.html
X-Download-Options: noopen

Content-Security-Policy

Content Security Policy ist ein extrem mächtiges Werkzeug. Ich kann hier nur einen kurzen Abriss geben was man damit alles machen kann.

1
Content-Security-Policy: default-src 'self'

Wie der Name default-src schon vermuten lässt, bedeutet dies, dass für alle sourcen (child-src, connect-src, font-src, img-src, media-src, object-src, script-src und style-src) die nachfolgend angegebene Policy gilt. In unserem Fall ‘self’, also dürfen alle Sourcen nur von der selben Quelle kommen, wie die Seite selbst.

1
Content-Security-Policy: default-src 'self'; script-src example.com

In diesem Fall gilt das Gleiche wie oben, mit der Ausnahme, dass die Scripte auch von der Seite example.com geladen werden können.

1
Content-Security-Policy-Report-Only: default-src 'self'; report-uri http://loghost.example.com/reports.js

Man kann die Content-Security-Policy Einstellungen auch einfach mal testen. Wenn man nicht die Content-Security-Policy sondern eine Content-Security-Policy-Report-Only angibt, so verhindert der Browser das Laden einer nicht erlaubten Ressource nicht, allerdings reportet er einen Verstoß an die angegebene report-uri.

Umsetzung als Security Aspect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ContentSecurityPolicyAspect implements SecurityAspect {
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
//response.setHeader("Content-Security-Policy", "default-src 'self'");
//response.setHeader("Content-Security-Policy", "default-src 'self'; report-uri /cspreporter");
response.setHeader("Content-Security-Policy-Report-Only", "default-src 'self'; report-uri "+request.getContextPath()+"/cspreporter");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}

Servlet um die Verstöße zu Loggen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@WebServlet("/cspreporter")
public class ContentSecurityPolicyReporter extends HttpServlet {
private static final Logger LOG = Logger.getLogger(ContentSecurityPolicyReporter.class.getName());
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("text/plain");
StringBuilder buffer = new StringBuilder();
BufferedReader reader = req.getReader();
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
String input = buffer.toString();
LOG.log(Level.WARNING, "CSP Report:\n" + input);
resp.getWriter().println("Reported.");
}
}

Mehr infos zu CSP gibt es hier und hier.

Cache-Control

Jeder Browser verfügt über einen Cache, welcher Ressourcen speichern kann. Darin können Ressourcen, welche sich selten oder nie ändern, abgelegt werden und dabei kann man dem Browser noch mitteilen, wie er mit diesen Inhalten umgehen soll.

1
Cache-Control: no-cache

Diese Ressource darf nicht gecached werden. Der Browser muss diese Ressource jedes Mal neu Anfragen.

1
Cache-Control: public, max-age=31536000

Diese Ressource ist öffentlich für jeden User zugänglich. Also nicht auf eine Session begrenzt. Die Lebensdauer (max-age) ist ein Jahr (Angabe in Sekunden). Der Browser kann diese Ressource für ein Jahr speichern und muss diese Ressource kein weiteres Mal vom Server abholen.

1
Cache-Control: private, max-age=31536000

Diese Ressource darf auch für ein Jahr gespeichert werden, allerdings ist sie privat gekennzeichnet, da sie eventuell Userdaten beinhaltet.

1
2
Cache-Control: max-age=120
ETag: "x222df"

Diese Ressource ist nur 120 Sekunden gültig. Danach kann der Browser den Server mittels des ETags fragen, ob sich die Ressource verändert hat (ETag ist unterschiedlich). Hat sich die Ressource nicht verändert, dann wird sie nicht neu herunter geladen.

Man kann mit der Cache-Control Direktive eine Web-Seite wesentlich effizienter machen. Es werden weniger Serveranfragen gestellt. Der Übertragungsaufwand vom Server zum Client wird minimiert. Immer öfter sieht man die Variante, die max-age auf ein Jahr zu erhöhen. Dabei wird in den URL’s der Ressourcen eine Versionsnummer integriert. Domit wird bei jedem Verändern der Files eine neue Versionsnummer in die URL integriert und der Cache überlistet.

Umsetzung als Filter:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@WebFilter(
filterName = "CacheFilter",
urlPatterns = {"/*"},
initParams = {
@WebInitParam(name = "expiration", value = "6000")
}
)
public class CacheFilter implements Filter {
private long expiration;
private String cacheability = "public";
private boolean mustRevalidate;
private String vary;
@Override
public void doFilter(ServletRequest servletrequest, ServletResponse servletresponse, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletresponse;
StringBuilder cacheControl = new StringBuilder(cacheability).append(", max-age=").append(expiration);
if (mustRevalidate) {
cacheControl.append(", must-revalidate");
}
// Set cache directives
response.setHeader("Cache-Control", cacheControl.toString());
response.setDateHeader("Expires", System.currentTimeMillis() + expiration
* 1000L);
// Set Vary field
if (vary != null && !vary.isEmpty()) {
response.setHeader("Vary", vary);
}
chain.doFilter(servletrequest, servletresponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
try {
expiration = Long.valueOf(filterConfig.getInitParameter("expiration"));
} catch (NumberFormatException e) {
throw new ServletException(new StringBuilder("The initialization parameter ")
.append("expiration")
.append(" is invalid or is missing for the filter ").append(filterConfig.getFilterName())
.append(".").toString());
}
cacheability = Boolean.valueOf(filterConfig.getInitParameter("private")) ? "private" : "public";
mustRevalidate = Boolean.valueOf(filterConfig.getInitParameter("must-revalidate"));
vary = filterConfig.getInitParameter("vary");
}
@Override
public void destroy() {
}
}

Siehe auch hier.