Configuring Security Headers in Undertow
If you're interested in using security headers to help protect your websites, there are a number of approaches you can take. Let's explore some of the popular ones.
Join the DZone community and get the full member experience.
Join For FreeSecurity headers are an excellent way to reduce exploitations of your website. There are several important headers with varying levels of difficulty to implement. Implementing them with the strictest policies on new projects will help enforce better practices. Adding security headers to legacy projects can be a bit more work if you want to set a strict content security policy or your legacy site doesn't support HTTPS everywhere. It's fairly common to handle all security headers in a central load balancer or a proxy server like HAProxy or NGINX. This makes it much easier to maintain if you have different tech stacks and want a simple way to keep everything consistent. Since we only use Java and Undertow, let's look at how we added simple implementations for this site, which can be seen at the following PR's CSP and Security Headers. Scott Helme's securityheaders.io is a great online tool for verifying if you have implemented the headers correctly. The previous PR's brought our score from F
to A+
.
Strict-Transport-Security (HSTS)
The Strict-Transport-Security
header essentially tells the browser that once it has connected to a host over TLS and receives this header, all subsequent requests should be forced to be TLS as well up until the expiration. Since we should all be sending our traffic over HTTPS these days this is a no-brainer and very simple to implement if you already support HTTPS everywhere. For a much more detailed explanation see HSTS the missing link in TLS.
public class StrictTransportSecurityHandlers {
public static HttpHandler hsts(HttpHandler next, long maxAge) {
return new SetHeaderHandler(next, Headers.STRICT_TRANSPORT_SECURITY_STRING, "max-age=" + maxAge);
}
public static HttpHandler hstsIncludeSubdomains(HttpHandler next, long maxAge) {
return new SetHeaderHandler(next, Headers.STRICT_TRANSPORT_SECURITY_STRING, "max-age=" + maxAge + "; includeSubDomains");
}
}
Referrer-Policy
On the surface it doesn't seem like a referring URL has much to do with security but it could potentially leak some internal information. It's important to set the Referrer-Policy
on internal sites. For example as a tech blog occasionally we see referrer's from applications like Jira, Github, or even internal tools like Amazons' internal code review tool. Most of these URLs are internal only and cannot be accessed externally from the network. However, especially with vanity URLs, they may give away the company that is linking to us, a project name, and sometimes specifics about a particular bug. Couple this with a landing page and you can get more info than you might expect. A real example allowed us to identify a company (which could be figured out by the URL) had a bug related to enum parsing since the Jira task mentioned it was a bug in the URL and the landing page was Java Enum Lookup by Name or Field Without Throwing Exceptions. For the most part who really cares, but you could be leaking more information than you intend. What if you are linking from internal admin pages now attackers have some information to go off of. Find out specifics about each policy here. If you are a public facing site who drives traffic to external sites there are a few options to choose from that allow you to keep some information if you choose.
public class ReferrerPolicyHandlers {
private static final String REFERRER_POLICY_STRING = "Referrer-Policy";
// See https://scotthelme.co.uk/a-new-security-header-referrer-policy/
public enum ReferrerPolicy {
EMPTY(""),
NO_REFERRER("no-referrer"),
NO_REFERRER_WHEN_DOWNGRADE("no-referrer-when-downgrade"),
SAME_ORIGIN("same-origin"),
ORIGIN("origin"),
STRICT_ORIGIN("strict-origin"),
ORIGIN_WHEN_CROSS_ORIGIN("origin-when-cross-origin"),
STRICT_ORIGIN_WHEN_CROSS_ORIGIN("strict-origin-when-cross-origin"),
UNSAFE_URL("unsafe-url");
private final String value;
ReferrerPolicy(String value) {
this.value = value;
}
public String getValue() {
return value;
}
};
public static HttpHandler policy(HttpHandler next, ReferrerPolicy policy) {
return new SetHeaderHandler(next, REFERRER_POLICY_STRING, policy.getValue());
}
}
X-Content-Type-Options
This header only has one valid value of nosniff
. It basically tells the browser not to try and guess the content-type of a file and only trust the value the server sends (More Information on X-Content-Type-Options).
public class XContentTypeOptionsHandler {
private static final String X_CONTENT_TYPE_OPTIONS_STRING = "X-Content-Type-Options";
public static HttpHandler nosniff(HttpHandler next) {
return new SetHeaderHandler(next, X_CONTENT_TYPE_OPTIONS_STRING, "nosniff");
}
}
X-Frame-Options (Iframe Control)
The X-Frame-Options
allows you to tell the browser which hosts are allowed to load your site in an iframe. This can be important because an attacker could purchase a domain similar to your own, load your entire site in an iframe, then use CSS/JS to add overlaid forms or intercept certain events (more details). If you do not rely on iframes, it is probably safe to set this to always deny which will prevent other sites from loading your site in an iframe. The ALLOW-FROM
option only supports a single host, if you need it to support multiple hosts you will need a dynamic solution along the lines of checking the referrer against a whitelist and dynamically allowing it.
public class XFrameOptionsHandlers {
private static final String X_FRAME_OPTIONS_STRING = "X-Frame-Options";
private static final HttpString X_FRAME_OPTIONS = new HttpString(X_FRAME_OPTIONS_STRING);
public static HttpHandler deny(HttpHandler next) {
return new SetHeaderHandler(next, X_FRAME_OPTIONS_STRING, "DENY");
}
public static HttpHandler sameOrigin(HttpHandler next) {
return new SetHeaderHandler(next, X_FRAME_OPTIONS_STRING, "SAMEORIGIN");
}
public static HttpHandler allowFromOrigin(HttpHandler next, String origin) {
return new SetHeaderHandler(next, X_FRAME_OPTIONS_STRING, "ALLOW-FROM " + origin);
}
public static HttpHandler allowFromDynamicOrigin(HttpHandler next,
Function<HttpServerExchange, String> originExtractor) {
// Since this is dynamic skip using the SetHeaderHandler
return exchange -> {
exchange.getResponseHeaders().put(X_FRAME_OPTIONS, originExtractor.apply(exchange));
next.handleRequest(exchange);
};
}
}
X-XSS-Protection
This header will enable some built-in browser XSS protection with varying modes.
public class XXssProtectionHandlers {
private static final String X_XSS_PROTECTION_STRING = "X-Xss-Protection";
public static HttpHandler disable(HttpHandler next) {
return new SetHeaderHandler(next, X_XSS_PROTECTION_STRING, "0");
}
public static HttpHandler enable(HttpHandler next) {
return new SetHeaderHandler(next, X_XSS_PROTECTION_STRING, "1");
}
public static HttpHandler enableAndBlock(HttpHandler next) {
return new SetHeaderHandler(next, X_XSS_PROTECTION_STRING, "1; mode=block");
}
}
Content-Security-Policy (CSP)
The Content-Security-Policy
header is a way to lock down what types of resources are allowed to be loaded from specific sources. This can be very finely controlled or use broader defaults available CSP options. This header is great to set for early-stage projects but can be quite a bit more of a chore for legacy sites. In order to set very strict rules, you need to ensure your site doesn't have any inline scripting (including Google Analytics! See here) as well as no inline CSS styles (yikes!). Since this header can be quite verbose, make sure it is only set on pages that load HTML documents. This header is essentially useless on things like JSON APIs and can add quite a few bytes per request which could add up for high traffic endpoints.
public class ContentSecurityPolicyHandler {
private static final String CSP_HEADER = "Content-Security-Policy";
public enum ContentSecurityPolicy {
NONE("'none'"), // blocks the use of this type of resource.
SELF("'self'"), // matches the current origin (but not subdomains).
UNSAFE_INLINE("'unsafe-inline'"), // allows the use of inline JS and CSS.
UNSAFE_EVAL("'unsafe-eval'"), // allows the use of mechanisms like eval().
;
private final String value;
ContentSecurityPolicy(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
// https://scotthelme.co.uk/content-security-policy-an-introduction/#whatcanweprotect
public static class Builder {
private final Map<String, String> policyMap;
public Builder() {
this.policyMap = new HashMap<>();
}
public Builder defaultSrc(ContentSecurityPolicy policy) {
policyMap.put("default-src", policy.getValue());
return this;
}
public Builder defaultSrc(String... policies) {
policyMap.put("default-src", join(policies));
return this;
}
public Builder scriptSrc(ContentSecurityPolicy policy) {
policyMap.put("script-src", policy.getValue());
return this;
}
public Builder scriptSrc(String... policies) {
policyMap.put("script-src", join(policies));
return this;
}
public Builder objectSrc(ContentSecurityPolicy policy) {
policyMap.put("object-src", policy.getValue());
return this;
}
public Builder objectSrc(String... policies) {
policyMap.put("object-src", join(policies));
return this;
}
public Builder styleSrc(ContentSecurityPolicy policy) {
policyMap.put("style-src", policy.getValue());
return this;
}
public Builder styleSrc(String... policies) {
policyMap.put("style-src", join(policies));
return this;
}
public Builder imgSrc(ContentSecurityPolicy policy) {
policyMap.put("img-src", policy.getValue());
return this;
}
public Builder imgSrc(String... policies) {
policyMap.put("img-src", join(policies));
return this;
}
public Builder mediaSrc(ContentSecurityPolicy policy) {
policyMap.put("media-src", policy.getValue());
return this;
}
public Builder mediaSrc(String... policies) {
policyMap.put("media-src", join(policies));
return this;
}
public Builder frameSrc(ContentSecurityPolicy policy) {
policyMap.put("frame-src", policy.getValue());
return this;
}
public Builder frameSrc(String... policies) {
policyMap.put("frame-src", join(policies));
return this;
}
public Builder fontSrc(ContentSecurityPolicy policy) {
policyMap.put("font-src", policy.getValue());
return this;
}
public Builder fontSrc(String... policies) {
policyMap.put("font-src", join(policies));
return this;
}
public Builder connectSrc(ContentSecurityPolicy policy) {
policyMap.put("connect-src", policy.getValue());
return this;
}
public Builder connectSrc(String... policies) {
policyMap.put("connect-src", join(policies));
return this;
}
public Builder formAction(ContentSecurityPolicy policy) {
policyMap.put("form-action", policy.getValue());
return this;
}
public Builder formAction(String... policies) {
policyMap.put("form-action", join(policies));
return this;
}
public Builder sandbox(ContentSecurityPolicy policy) {
policyMap.put("sandbox", policy.getValue());
return this;
}
public Builder sandbox(String... policies) {
policyMap.put("sandbox", join(policies));
return this;
}
public Builder scriptNonce(ContentSecurityPolicy policy) {
policyMap.put("script-nonce", policy.getValue());
return this;
}
public Builder scriptNonce(String... policies) {
policyMap.put("script-nonce", join(policies));
return this;
}
public Builder pluginTypes(ContentSecurityPolicy policy) {
policyMap.put("plugin-types", policy.getValue());
return this;
}
public Builder pluginTypes(String... policies) {
policyMap.put("plugin-types", join(policies));
return this;
}
public Builder reflectedXss(ContentSecurityPolicy policy) {
policyMap.put("reflected-xss", policy.getValue());
return this;
}
public Builder reflectedXss(String... policies) {
policyMap.put("reflected-xss", join(policies));
return this;
}
public Builder reportUri(String uri) {
policyMap.put("report-uri", uri);
return this;
}
public HttpHandler build(HttpHandler delegate) {
String policy = policyMap.entrySet()
.stream()
.map(entry -> entry.getKey() + " " + entry.getValue())
.collect(Collectors.joining("; "));
return new SetHeaderHandler(delegate, CSP_HEADER, policy);
}
private String join(String... strings) {
return Stream.of(strings).collect(Collectors.joining(" "));
}
}
}
Here are is the CSP for StubbornJava.
private static HttpHandler contentSecurityPolicy(HttpHandler delegate) {
return new ContentSecurityPolicyHandler.Builder()
.defaultSrc(ContentSecurityPolicy.SELF)
.scriptSrc(ContentSecurityPolicy.SELF.getValue(), "https://www.google-analytics.com")
// Drop the wildcard when we host our own images.
.imgSrc(ContentSecurityPolicy.SELF.getValue(), "https://www.google-analytics.com", "*")
.connectSrc(ContentSecurityPolicy.SELF.getValue(), "https://www.google-analytics.com")
.fontSrc(ContentSecurityPolicy.SELF.getValue(), "data:")
.styleSrc(ContentSecurityPolicy.SELF.getValue(), ContentSecurityPolicy.UNSAFE_INLINE.getValue())
.build(delegate);
}
Here is our convenience HttpHandler
for setting security headers across our applications. CSP was left out since it's more likely to be customized.
public static HttpHandler securityHeaders(HttpHandler next, ReferrerPolicy policy) {
MiddlewareBuilder security = MiddlewareBuilder
.begin(XFrameOptionsHandlers::deny)
.next(XXssProtectionHandlers::enableAndBlock)
.next(XContentTypeOptionsHandler::nosniff)
.next(handler -> ReferrerPolicyHandlers.policy(handler, policy));
// TODO: Only add HSTS if we are not local. We should probably
// use a self signed cert locally for a better test env
if (Env.LOCAL != Env.get()) {
security = security.next(handler -> StrictTransportSecurityHandlers.hstsIncludeSubdomains(handler, 31536000L));
}
return security.complete(next);
}
Both our CSP handler and security headers handler can be seen early on in our Middleware.
private static HttpHandler wrapWithMiddleware(HttpHandler next) {
return MiddlewareBuilder.begin(PageRoutes::redirector)
.next(handler -> CustomHandlers.securityHeaders(handler, ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
.next(StubbornJavaWebApp::contentSecurityPolicy)
.next(CustomHandlers::gzip)
.next(BlockingHandler::new)
.next(ex -> CustomHandlers.accessLog(ex, logger))
.next(CustomHandlers::statusCodeMetrics)
.next(StubbornJavaWebApp::exceptionHandler)
.complete(next);
}
Recommendations
Published at DZone with permission of Bill O'Neil. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments