From aa23d49fc8b9c2537529dbdc13512000e2ab295a Mon Sep 17 00:00:00 2001 From: Robert Kanter Date: Wed, 23 May 2018 10:23:17 -0700 Subject: [PATCH] HADOOP-15457. Add Security-Related HTTP Response Header in WEBUIs. (kanwaljeets via rkanter) --- .../org/apache/hadoop/http/HttpServer2.java | 79 ++++++++++++++----- .../apache/hadoop/http/TestHttpServer.java | 61 ++++++++++++++ 2 files changed, 121 insertions(+), 19 deletions(-) diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java index 47ca8419d6..c273c7852b 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java @@ -34,6 +34,8 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -172,10 +174,16 @@ public final class HttpServer2 implements FilterContainer { private final SignerSecretProvider secretProvider; private XFrameOption xFrameOption; private boolean xFrameOptionIsEnabled; - private static final String X_FRAME_VALUE = "xFrameOption"; - private static final String X_FRAME_ENABLED = "X_FRAME_ENABLED"; - - + public static final String HTTP_HEADER_PREFIX = "hadoop.http.header."; + private static final String HTTP_HEADER_REGEX = + "hadoop\\.http\\.header\\.([a-zA-Z\\-_]+)"; + static final String X_XSS_PROTECTION = + "X-XSS-Protection:1; mode=block"; + static final String X_CONTENT_TYPE_OPTIONS = + "X-Content-Type-Options:nosniff"; + private static final String X_FRAME_OPTIONS = "X-FRAME-OPTIONS"; + private static final Pattern PATTERN_HTTP_HEADER_REGEX = + Pattern.compile(HTTP_HEADER_REGEX); /** * Class to construct instances of HTTP server with specific options. */ @@ -574,10 +582,7 @@ private void initializeWebServer(String name, String hostName, addDefaultApps(contexts, appDir, conf); webServer.setHandler(handlers); - Map xFrameParams = new HashMap<>(); - xFrameParams.put(X_FRAME_ENABLED, - String.valueOf(this.xFrameOptionIsEnabled)); - xFrameParams.put(X_FRAME_VALUE, this.xFrameOption.toString()); + Map xFrameParams = setHeaders(conf); addGlobalFilter("safety", QuotingInputFilter.class.getName(), xFrameParams); final FilterInitializer[] initializers = getFilterInitializers(conf); if (initializers != null) { @@ -1475,9 +1480,11 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) public static class QuotingInputFilter implements Filter { private FilterConfig config; + private Map headerMap; public static class RequestQuoter extends HttpServletRequestWrapper { private final HttpServletRequest rawRequest; + public RequestQuoter(HttpServletRequest rawRequest) { super(rawRequest); this.rawRequest = rawRequest; @@ -1566,6 +1573,7 @@ public String getServerName() { @Override public void init(FilterConfig config) throws ServletException { this.config = config; + initHttpHeaderMap(); } @Override @@ -1593,11 +1601,7 @@ public void doFilter(ServletRequest request, } else if (mime.startsWith("application/xml")) { httpResponse.setContentType("text/xml; charset=utf-8"); } - - if(Boolean.valueOf(this.config.getInitParameter(X_FRAME_ENABLED))) { - httpResponse.addHeader("X-FRAME-OPTIONS", - this.config.getInitParameter(X_FRAME_VALUE)); - } + headerMap.forEach((k, v) -> httpResponse.addHeader(k, v)); chain.doFilter(quoted, httpResponse); } @@ -1613,14 +1617,25 @@ private String inferMimeType(ServletRequest request) { return (mime == null) ? null : mime; } + private void initHttpHeaderMap() { + Enumeration params = this.config.getInitParameterNames(); + headerMap = new HashMap<>(); + while (params.hasMoreElements()) { + String key = params.nextElement(); + Matcher m = PATTERN_HTTP_HEADER_REGEX.matcher(key); + if (m.matches()) { + String headerKey = m.group(1); + headerMap.put(headerKey, config.getInitParameter(key)); + } + } + } } - - /** - * The X-FRAME-OPTIONS header in HTTP response to mitigate clickjacking - * attack. - */ + /** + * The X-FRAME-OPTIONS header in HTTP response to mitigate clickjacking + * attack. + */ public enum XFrameOption { - DENY("DENY") , SAMEORIGIN ("SAMEORIGIN"), ALLOWFROM ("ALLOW-FROM"); + DENY("DENY"), SAMEORIGIN("SAMEORIGIN"), ALLOWFROM("ALLOW-FROM"); XFrameOption(String name) { this.name = name; @@ -1651,4 +1666,30 @@ private static XFrameOption getEnum(String value) { throw new IllegalArgumentException("Unexpected value in xFrameOption."); } } + + + private Map setHeaders(Configuration conf) { + Map xFrameParams = new HashMap<>(); + Map headerConfigMap = + conf.getValByRegex(HTTP_HEADER_REGEX); + + xFrameParams.putAll(getDefaultHeaders()); + if(this.xFrameOptionIsEnabled) { + xFrameParams.put(HTTP_HEADER_PREFIX+X_FRAME_OPTIONS, + this.xFrameOption.toString()); + } + xFrameParams.putAll(headerConfigMap); + return xFrameParams; + } + + private Map getDefaultHeaders() { + Map headers = new HashMap<>(); + String[] splitVal = X_CONTENT_TYPE_OPTIONS.split(":"); + headers.put(HTTP_HEADER_PREFIX + splitVal[0], + splitVal[1]); + splitVal = X_XSS_PROTECTION.split(":"); + headers.put(HTTP_HEADER_PREFIX + splitVal[0], + splitVal[1]); + return headers; + } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java index 6c1512edc6..26b1137e49 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java @@ -701,4 +701,65 @@ public void testBacklogSize() throws Exception ServerConnector listener = (ServerConnector)listeners.get(0); assertEquals(backlogSize, listener.getAcceptQueueSize()); } + + @Test + public void testHttpResponseDefaultHeaders() throws Exception { + Configuration conf = new Configuration(); + HttpServer2 httpServer = createTestServer(conf); + try { + HttpURLConnection conn = getHttpURLConnection(httpServer); + assertEquals(HttpServer2.X_XSS_PROTECTION.split(":")[1], + conn.getHeaderField( + HttpServer2.X_XSS_PROTECTION.split(":")[0])); + assertEquals(HttpServer2.X_CONTENT_TYPE_OPTIONS.split(":")[1], + conn.getHeaderField( + HttpServer2.X_CONTENT_TYPE_OPTIONS.split(":")[0])); + } finally { + httpServer.stop(); + } + } + + @Test + public void testHttpResponseOverrideDefaultHeaders() throws Exception { + Configuration conf = new Configuration(); + conf.set(HttpServer2.HTTP_HEADER_PREFIX+ + HttpServer2.X_XSS_PROTECTION.split(":")[0], "customXssValue"); + HttpServer2 httpServer = createTestServer(conf); + try { + HttpURLConnection conn = getHttpURLConnection(httpServer); + assertEquals("customXssValue", + conn.getHeaderField( + HttpServer2.X_XSS_PROTECTION.split(":")[0]) + ); + assertEquals(HttpServer2.X_CONTENT_TYPE_OPTIONS.split(":")[1], + conn.getHeaderField( + HttpServer2.X_CONTENT_TYPE_OPTIONS.split(":")[0]) + ); + } finally { + httpServer.stop(); + } + } + + @Test + public void testHttpResponseCustomHeaders() throws Exception { + Configuration conf = new Configuration(); + String key = "customKey"; + String value = "customValue"; + conf.set(HttpServer2.HTTP_HEADER_PREFIX+key, value); + HttpServer2 httpServer = createTestServer(conf); + try { + HttpURLConnection conn = getHttpURLConnection(httpServer); + assertEquals(HttpServer2.X_XSS_PROTECTION.split(":")[1], + conn.getHeaderField( + HttpServer2.X_XSS_PROTECTION.split(":")[0])); + assertEquals(HttpServer2.X_CONTENT_TYPE_OPTIONS.split(":")[1], + conn.getHeaderField( + HttpServer2.X_CONTENT_TYPE_OPTIONS.split(":")[0])); + assertEquals(value, conn.getHeaderField( + key)); + } finally { + httpServer.stop(); + } + } + }