Skip to content

Commit 978546b

Browse files
anmolnarbrfrn169
authored andcommitted
HBASE-23303 Add security headers to REST server/info page (#843)
Signed-off-by: Toshihiro Suzuki <brfrn169@gmail.com> Signed-off-by: Sean Busbey <busbey@apache.org>
1 parent 60d9430 commit 978546b

File tree

6 files changed

+355
-26
lines changed

6 files changed

+355
-26
lines changed

hbase-http/src/main/java/org/apache/hadoop/hbase/http/ClickjackingPreventionFilter.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package org.apache.hadoop.hbase.http;
1919

2020
import java.io.IOException;
21+
import java.util.HashMap;
22+
import java.util.Map;
2123

2224
import javax.servlet.Filter;
2325
import javax.servlet.FilterChain;
@@ -27,13 +29,15 @@
2729
import javax.servlet.ServletResponse;
2830
import javax.servlet.http.HttpServletResponse;
2931

32+
import org.apache.hadoop.conf.Configuration;
3033
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
3134

3235
import org.apache.yetus.audience.InterfaceAudience;
3336

3437
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
3538
public class ClickjackingPreventionFilter implements Filter {
3639
private FilterConfig filterConfig;
40+
private static final String DEFAULT_XFRAMEOPTIONS = "DENY";
3741

3842
@Override
3943
public void init(FilterConfig filterConfig) throws ServletException {
@@ -51,4 +55,11 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
5155
@Override
5256
public void destroy() {
5357
}
58+
59+
public static Map<String, String> getDefaultParameters(Configuration conf) {
60+
Map<String, String> params = new HashMap<>();
61+
params.put("xframeoptions", conf.get("hbase.http.filter.xframeoptions.mode",
62+
DEFAULT_XFRAMEOPTIONS));
63+
return params;
64+
}
5465
}

hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,10 +596,15 @@ private void initializeWebServer(String name, String hostName,
596596
addDefaultApps(contexts, appDir, conf);
597597

598598
addGlobalFilter("safety", QuotingInputFilter.class.getName(), null);
599-
Map<String, String> params = new HashMap<>();
600-
params.put("xframeoptions", conf.get("hbase.http.filter.xframeoptions.mode", "DENY"));
599+
601600
addGlobalFilter("clickjackingprevention",
602-
ClickjackingPreventionFilter.class.getName(), params);
601+
ClickjackingPreventionFilter.class.getName(),
602+
ClickjackingPreventionFilter.getDefaultParameters(conf));
603+
604+
addGlobalFilter("securityheaders",
605+
SecurityHeadersFilter.class.getName(),
606+
SecurityHeadersFilter.getDefaultParameters(conf));
607+
603608
final FilterInitializer[] initializers = getFilterInitializers(conf);
604609
if (initializers != null) {
605610
conf = new Configuration(conf);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.hbase.http;
20+
21+
import java.io.IOException;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import javax.servlet.Filter;
25+
import javax.servlet.FilterChain;
26+
import javax.servlet.FilterConfig;
27+
import javax.servlet.ServletException;
28+
import javax.servlet.ServletRequest;
29+
import javax.servlet.ServletResponse;
30+
import javax.servlet.http.HttpServletResponse;
31+
import org.apache.commons.lang3.StringUtils;
32+
import org.apache.hadoop.conf.Configuration;
33+
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
34+
import org.apache.yetus.audience.InterfaceAudience;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
38+
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
39+
public class SecurityHeadersFilter implements Filter {
40+
private static final Logger LOG =
41+
LoggerFactory.getLogger(SecurityHeadersFilter.class);
42+
private static final String DEFAULT_HSTS = "";
43+
private static final String DEFAULT_CSP = "";
44+
private FilterConfig filterConfig;
45+
46+
@Override
47+
public void init(FilterConfig filterConfig) throws ServletException {
48+
this.filterConfig = filterConfig;
49+
LOG.info("Added security headers filter");
50+
}
51+
52+
@Override
53+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
54+
throws IOException, ServletException {
55+
HttpServletResponse httpResponse = (HttpServletResponse) response;
56+
httpResponse.addHeader("X-Content-Type-Options", "nosniff");
57+
httpResponse.addHeader("X-XSS-Protection", "1; mode=block");
58+
String hsts = filterConfig.getInitParameter("hsts");
59+
if (StringUtils.isNotBlank(hsts)) {
60+
httpResponse.addHeader("Strict-Transport-Security", hsts);
61+
}
62+
String csp = filterConfig.getInitParameter("csp");
63+
if (StringUtils.isNotBlank(csp)) {
64+
httpResponse.addHeader("Content-Security-Policy", csp);
65+
}
66+
chain.doFilter(request, response);
67+
}
68+
69+
@Override
70+
public void destroy() {
71+
}
72+
73+
public static Map<String, String> getDefaultParameters(Configuration conf) {
74+
Map<String, String> params = new HashMap<>();
75+
params.put("hsts", conf.get("hbase.http.filter.hsts.value",
76+
DEFAULT_HSTS));
77+
params.put("csp", conf.get("hbase.http.filter.csp.value",
78+
DEFAULT_CSP));
79+
return params;
80+
}
81+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.http;
19+
20+
import static org.apache.hadoop.hbase.http.HttpServerFunctionalTest.createTestServer;
21+
import static org.apache.hadoop.hbase.http.HttpServerFunctionalTest.getServerURL;
22+
import static org.hamcrest.CoreMatchers.equalTo;
23+
import static org.hamcrest.CoreMatchers.is;
24+
import static org.hamcrest.CoreMatchers.not;
25+
import static org.junit.Assert.assertThat;
26+
import java.io.IOException;
27+
import java.net.HttpURLConnection;
28+
import java.net.URL;
29+
import org.apache.hadoop.conf.Configuration;
30+
import org.apache.hadoop.hbase.HBaseClassTestRule;
31+
import org.apache.hadoop.hbase.testclassification.MediumTests;
32+
import org.hamcrest.core.Is;
33+
import org.hamcrest.core.IsEqual;
34+
import org.junit.After;
35+
import org.junit.ClassRule;
36+
import org.junit.Test;
37+
import org.junit.experimental.categories.Category;
38+
39+
@Category({HttpServerFunctionalTest.class, MediumTests.class})
40+
public class TestSecurityHeadersFilter {
41+
private static URL baseUrl;
42+
private HttpServer http;
43+
44+
@ClassRule
45+
public static final HBaseClassTestRule CLASS_RULE =
46+
HBaseClassTestRule.forClass(TestSecurityHeadersFilter.class);
47+
48+
@After
49+
public void tearDown() throws Exception {
50+
http.stop();
51+
}
52+
53+
@Test
54+
public void testDefaultValues() throws Exception {
55+
http = createTestServer();
56+
http.start();
57+
baseUrl = getServerURL(http);
58+
59+
URL url = new URL(baseUrl, "/");
60+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
61+
assertThat(conn.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
62+
63+
assertThat("Header 'X-Content-Type-Options' is missing",
64+
conn.getHeaderField("X-Content-Type-Options"), is(not((String)null)));
65+
assertThat(conn.getHeaderField("X-Content-Type-Options"), equalTo("nosniff"));
66+
assertThat("Header 'X-XSS-Protection' is missing",
67+
conn.getHeaderField("X-XSS-Protection"), is(not((String)null)));
68+
assertThat("Header 'X-XSS-Protection' has invalid value",
69+
conn.getHeaderField("X-XSS-Protection"), equalTo("1; mode=block"));
70+
71+
assertThat("Header 'Strict-Transport-Security' should be missing from response," +
72+
"but it's present",
73+
conn.getHeaderField("Strict-Transport-Security"), is((String)null));
74+
assertThat("Header 'Content-Security-Policy' should be missing from response," +
75+
"but it's present",
76+
conn.getHeaderField("Content-Security-Policy"), is((String)null));
77+
}
78+
79+
@Test
80+
public void testHstsAndCspSettings() throws IOException {
81+
Configuration conf = new Configuration();
82+
conf.set("hbase.http.filter.hsts.value",
83+
"max-age=63072000;includeSubDomains;preload");
84+
conf.set("hbase.http.filter.csp.value",
85+
"default-src https: data: 'unsafe-inline' 'unsafe-eval'");
86+
http = createTestServer(conf);
87+
http.start();
88+
baseUrl = getServerURL(http);
89+
90+
URL url = new URL(baseUrl, "/");
91+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
92+
assertThat(conn.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
93+
94+
assertThat("Header 'Strict-Transport-Security' is missing from Rest response",
95+
conn.getHeaderField("Strict-Transport-Security"), Is.is(not((String)null)));
96+
assertThat("Header 'Strict-Transport-Security' has invalid value",
97+
conn.getHeaderField("Strict-Transport-Security"),
98+
IsEqual.equalTo("max-age=63072000;includeSubDomains;preload"));
99+
100+
assertThat("Header 'Content-Security-Policy' is missing from Rest response",
101+
conn.getHeaderField("Content-Security-Policy"), Is.is(not((String)null)));
102+
assertThat("Header 'Content-Security-Policy' has invalid value",
103+
conn.getHeaderField("Content-Security-Policy"),
104+
IsEqual.equalTo("default-src https: data: 'unsafe-inline' 'unsafe-eval'"));
105+
}
106+
}

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,59 +18,56 @@
1818

1919
package org.apache.hadoop.hbase.rest;
2020

21+
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
2122
import java.lang.management.ManagementFactory;
2223
import java.util.ArrayList;
24+
import java.util.EnumSet;
2325
import java.util.List;
2426
import java.util.Map;
25-
import java.util.EnumSet;
2627
import java.util.concurrent.ArrayBlockingQueue;
27-
28-
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
28+
import javax.servlet.DispatcherType;
2929
import org.apache.commons.lang3.ArrayUtils;
30-
import org.apache.yetus.audience.InterfaceAudience;
3130
import org.apache.hadoop.conf.Configuration;
3231
import org.apache.hadoop.hbase.HBaseConfiguration;
3332
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
33+
import org.apache.hadoop.hbase.http.ClickjackingPreventionFilter;
34+
import org.apache.hadoop.hbase.http.HttpServerUtil;
3435
import org.apache.hadoop.hbase.http.InfoServer;
36+
import org.apache.hadoop.hbase.http.SecurityHeadersFilter;
3537
import org.apache.hadoop.hbase.log.HBaseMarkers;
3638
import org.apache.hadoop.hbase.rest.filter.AuthFilter;
3739
import org.apache.hadoop.hbase.rest.filter.GzipFilter;
3840
import org.apache.hadoop.hbase.rest.filter.RestCsrfPreventionFilter;
3941
import org.apache.hadoop.hbase.security.UserProvider;
4042
import org.apache.hadoop.hbase.util.DNS;
41-
import org.apache.hadoop.hbase.http.HttpServerUtil;
4243
import org.apache.hadoop.hbase.util.Pair;
4344
import org.apache.hadoop.hbase.util.ReflectionUtils;
4445
import org.apache.hadoop.hbase.util.Strings;
4546
import org.apache.hadoop.hbase.util.VersionInfo;
46-
47-
import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
48-
import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
49-
import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter;
50-
import org.apache.hbase.thirdparty.org.apache.commons.cli.Options;
51-
import org.apache.hbase.thirdparty.org.apache.commons.cli.ParseException;
52-
import org.apache.hbase.thirdparty.org.apache.commons.cli.PosixParser;
53-
47+
import org.apache.yetus.audience.InterfaceAudience;
5448
import org.eclipse.jetty.http.HttpVersion;
55-
import org.eclipse.jetty.server.Server;
56-
import org.eclipse.jetty.server.HttpConnectionFactory;
57-
import org.eclipse.jetty.server.SslConnectionFactory;
49+
import org.eclipse.jetty.jmx.MBeanContainer;
5850
import org.eclipse.jetty.server.HttpConfiguration;
59-
import org.eclipse.jetty.server.ServerConnector;
51+
import org.eclipse.jetty.server.HttpConnectionFactory;
6052
import org.eclipse.jetty.server.SecureRequestCustomizer;
61-
import org.eclipse.jetty.util.ssl.SslContextFactory;
53+
import org.eclipse.jetty.server.Server;
54+
import org.eclipse.jetty.server.ServerConnector;
55+
import org.eclipse.jetty.server.SslConnectionFactory;
56+
import org.eclipse.jetty.servlet.FilterHolder;
6257
import org.eclipse.jetty.servlet.ServletContextHandler;
6358
import org.eclipse.jetty.servlet.ServletHolder;
59+
import org.eclipse.jetty.util.ssl.SslContextFactory;
6460
import org.eclipse.jetty.util.thread.QueuedThreadPool;
65-
import org.eclipse.jetty.jmx.MBeanContainer;
66-
import org.eclipse.jetty.servlet.FilterHolder;
67-
6861
import org.glassfish.jersey.server.ResourceConfig;
6962
import org.glassfish.jersey.servlet.ServletContainer;
7063
import org.slf4j.Logger;
7164
import org.slf4j.LoggerFactory;
72-
73-
import javax.servlet.DispatcherType;
65+
import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
66+
import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
67+
import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter;
68+
import org.apache.hbase.thirdparty.org.apache.commons.cli.Options;
69+
import org.apache.hbase.thirdparty.org.apache.commons.cli.ParseException;
70+
import org.apache.hbase.thirdparty.org.apache.commons.cli.PosixParser;
7471

7572
/**
7673
* Main class for launching REST gateway as a servlet hosted by Jetty.
@@ -137,6 +134,23 @@ void addCSRFFilter(ServletContextHandler ctxHandler, Configuration conf) {
137134
}
138135
}
139136

137+
private void addClickjackingPreventionFilter(ServletContextHandler ctxHandler,
138+
Configuration conf) {
139+
FilterHolder holder = new FilterHolder();
140+
holder.setName("clickjackingprevention");
141+
holder.setClassName(ClickjackingPreventionFilter.class.getName());
142+
holder.setInitParameters(ClickjackingPreventionFilter.getDefaultParameters(conf));
143+
ctxHandler.addFilter(holder, PATH_SPEC_ANY, EnumSet.allOf(DispatcherType.class));
144+
}
145+
146+
private void addSecurityHeadersFilter(ServletContextHandler ctxHandler, Configuration conf) {
147+
FilterHolder holder = new FilterHolder();
148+
holder.setName("securityheaders");
149+
holder.setClassName(SecurityHeadersFilter.class.getName());
150+
holder.setInitParameters(SecurityHeadersFilter.getDefaultParameters(conf));
151+
ctxHandler.addFilter(holder, PATH_SPEC_ANY, EnumSet.allOf(DispatcherType.class));
152+
}
153+
140154
// login the server principal (if using secure Hadoop)
141155
private static Pair<FilterHolder, Class<? extends ServletContainer>> loginServerPrincipal(
142156
UserProvider userProvider, Configuration conf) throws Exception {
@@ -349,6 +363,8 @@ public synchronized void run() throws Exception {
349363
ctxHandler.addFilter(filter, PATH_SPEC_ANY, EnumSet.of(DispatcherType.REQUEST));
350364
}
351365
addCSRFFilter(ctxHandler, conf);
366+
addClickjackingPreventionFilter(ctxHandler, conf);
367+
addSecurityHeadersFilter(ctxHandler, conf);
352368
HttpServerUtil.constrainHttpMethods(ctxHandler, servlet.getConfiguration()
353369
.getBoolean(REST_HTTP_ALLOW_OPTIONS_METHOD, REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT));
354370

0 commit comments

Comments
 (0)