diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index ff5c858..afd7c70 100644 --- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -2395,6 +2395,9 @@ private static void populateLlapDaemonVarsSet(Set llapDaemonVarsSetLocal HIVE_SERVER2_THRIFT_RESULTSET_MAX_FETCH_SIZE("hive.server2.thrift.resultset.max.fetch.size", 1000, "Max number of rows sent in one Fetch RPC call by the server to the client."), + HIVE_SERVER2_XSRF_FILTER_ENABLED("hive.server2.xsrf.filter.enabled",false, + "If enabled, HiveServer2 will block any requests made to it over http " + + "if an X-XSRF-HEADER header is not present"), HIVE_SECURITY_COMMAND_WHITELIST("hive.security.command.whitelist", "set,reset,dfs,add,list,delete,reload,compile", "Comma separated list of non-SQL Hive commands users are authorized to execute"), @@ -2935,7 +2938,8 @@ private static void populateLlapDaemonVarsSet(Set llapDaemonVarsSetLocal HIVE_CONF_RESTRICTED_LIST("hive.conf.restricted.list", - "hive.security.authenticator.manager,hive.security.authorization.manager,hive.users.in.admin.role", + "hive.security.authenticator.manager,hive.security.authorization.manager,hive.users.in.admin.role," + + "hive.server2.xsrf.filter.enabled", "Comma separated list of configuration options which are immutable at runtime"), HIVE_CONF_HIDDEN_LIST("hive.conf.hidden.list", METASTOREPWD.varname + "," + HIVE_SERVER2_SSL_KEYSTORE_PASSWORD.varname, diff --git a/hcatalog/webhcat/svr/src/main/config/webhcat-default.xml b/hcatalog/webhcat/svr/src/main/config/webhcat-default.xml index e33cf8c..fa8dbf8 100644 --- a/hcatalog/webhcat/svr/src/main/config/webhcat-default.xml +++ b/hcatalog/webhcat/svr/src/main/config/webhcat-default.xml @@ -363,4 +363,12 @@ --> + + templeton.xsrf.filter.enabled + false + + Enable XSRF protection - looks for the presence of a X-XSRF-HEADER header + in all PUT/POST requests, and rejects requests that do not have these. + + diff --git a/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/AppConfig.java b/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/AppConfig.java index 161ab3b..10caf9b 100644 --- a/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/AppConfig.java +++ b/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/AppConfig.java @@ -168,6 +168,7 @@ */ public static final String HIVE_EXTRA_FILES = "templeton.hive.extra.files"; + public static final String XSRF_FILTER_ENABLED = "templeton.xsrf.filter.enabled"; private static final Logger LOG = LoggerFactory.getLogger(AppConfig.class); diff --git a/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/Main.java b/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/Main.java index d5fea1e..5208bf4 100644 --- a/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/Main.java +++ b/hcatalog/webhcat/svr/src/main/java/org/apache/hive/hcatalog/templeton/Main.java @@ -34,6 +34,7 @@ import org.apache.hadoop.hive.common.classification.InterfaceAudience; import org.apache.hadoop.hive.common.classification.InterfaceStability; import org.apache.hadoop.hdfs.web.AuthFilter; +import org.apache.hadoop.hive.shims.Utils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; import org.apache.hadoop.security.authentication.server.PseudoAuthenticationHandler; @@ -211,6 +212,14 @@ public Server runServer(int port) root.addFilter(fHolder, "/" + SERVLET_PATH + "/v1/version/*", FilterMapping.REQUEST); + if (conf.getBoolean(AppConfig.XSRF_FILTER_ENABLED, false)){ + root.addFilter(makeXSRFFilter(), "/" + SERVLET_PATH + "/*", + FilterMapping.REQUEST); + LOG.debug("XSRF filter enabled"); + } else { + LOG.warn("XSRF filter disabled"); + } + // Connect Jersey ServletHolder h = new ServletHolder(new ServletContainer(makeJerseyConfig())); root.addServlet(h, "/" + SERVLET_PATH + "/*"); @@ -223,6 +232,21 @@ public Server runServer(int port) return server; } + public FilterHolder makeXSRFFilter() { + String customHeader = null; // The header to look for. We use "X-XSRF-HEADER" if this is null. + String methodsToIgnore = null; // Methods to not filter. By default: "GET,OPTIONS,HEAD,TRACE" if null. + FilterHolder fHolder = new FilterHolder(Utils.getXSRFFilter()); + if (customHeader != null){ + fHolder.setInitParameter(Utils.XSRF_CUSTOM_HEADER_PARAM, customHeader); + } + if (methodsToIgnore != null){ + fHolder.setInitParameter(Utils.XSRF_CUSTOM_METHODS_TO_IGNORE_PARAM, methodsToIgnore); + } + FilterHolder xsrfFilter = fHolder; + + return xsrfFilter; + } + // Configure the AuthFilter with the Kerberos params iff security // is enabled. public FilterHolder makeAuthFilter() { diff --git a/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestXSRFFilter.java b/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestXSRFFilter.java new file mode 100644 index 0000000..2b0ffbe --- /dev/null +++ b/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestXSRFFilter.java @@ -0,0 +1,149 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hive.jdbc; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.conf.HiveConf.ConfVars; +import org.apache.hive.jdbc.miniHS2.MiniHS2; +import org.apache.hive.jdbc.XsrfHttpRequestInterceptor; +import org.junit.After; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class TestXSRFFilter { + + private static MiniHS2 miniHS2 = null; + private static String dataFileDir; + private static Path kvDataFilePath; + private static final String tmpDir = System.getProperty("test.tmp.dir"); + + private Connection hs2Conn = null; + + + // This is not modeled as a @Before, because it needs to be parameterized per-test. + // If there is a better way to do this, we should do it. + private void initHS2(boolean enableXSRFFilter) throws Exception { + Class.forName(MiniHS2.getJdbcDriverName()); + HiveConf conf = new HiveConf(); + conf.setBoolVar(ConfVars.HIVE_SUPPORT_CONCURRENCY, false); + miniHS2 = new MiniHS2(conf); + dataFileDir = conf.get("test.data.files").replace('\\', '/').replace("c:", ""); + kvDataFilePath = new Path(dataFileDir, "kv1.txt"); + Map confOverlay = new HashMap(); + confOverlay.put(ConfVars.HIVE_SERVER2_XSRF_FILTER_ENABLED.varname, String.valueOf(enableXSRFFilter)); + confOverlay.put(ConfVars.HIVE_SERVER2_TRANSPORT_MODE.varname, "http"); + miniHS2.start(confOverlay); + } + + private Connection getConnection(String jdbcURL, String user, String pwd) throws SQLException { + Connection conn = DriverManager.getConnection(jdbcURL, user, pwd); + conn.createStatement().execute("set hive.support.concurrency = false"); + return conn; + } + + @After + public void tearDownHS2() throws Exception { + if (hs2Conn != null){ + hs2Conn.close(); + hs2Conn = null; + } + if ((miniHS2!= null) && miniHS2.isStarted()) { + miniHS2.stop(); + miniHS2 = null; + } + } + + @Test + public void testFilterDisabledNoInjection() throws Exception { + // filter disabled, injection disabled, exception not expected + runTest(false,false); + } + + @Test + public void testFilterDisabledWithInjection() throws Exception { + // filter disabled, injection enabled, exception not expected + runTest(false,true); + } + + @Test + public void testFilterEnabledWithInjection() throws Exception { + // filter enabled, injection enabled, exception not expected + runTest(true,true); + } + + @Test + public void testFilterEnabledNoInjection() throws Exception { + // filter enabled, injection disabled, exception expected + runTest(true,false); + } + + private void runTest(boolean filterEnabled, boolean injectionEnabled) throws Exception { + // Exception is expected only if filter is enabled and injection is disabled + boolean exceptionExpected = filterEnabled && (!injectionEnabled); + initHS2(filterEnabled); + XsrfHttpRequestInterceptor.enableHeaderInjection(injectionEnabled); + Exception e = null; + try { + runBasicCommands(); + } catch (Exception thrown) { + e = thrown; + } + if (exceptionExpected){ + assertNotNull(e); + } else { + assertEquals(null,e); + } + } + + + private void runBasicCommands() throws Exception { + hs2Conn = getConnection(miniHS2.getJdbcURL(), System.getProperty("user.name"), "bar"); + String tableName = "testTab1"; + Statement stmt = hs2Conn.createStatement(); + + // create table + stmt.execute("DROP TABLE IF EXISTS " + tableName); + stmt.execute("CREATE TABLE " + tableName + + " (under_col INT COMMENT 'the under column', value STRING) COMMENT ' test table'"); + + // load data + stmt.execute("load data local inpath '" + + kvDataFilePath.toString() + "' into table " + tableName); + + ResultSet res = stmt.executeQuery("SELECT * FROM " + tableName); + assertTrue(res.next()); + assertEquals("val_238", res.getString(2)); + res.close(); + stmt.close(); + } + +} diff --git a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java index 0b0db43..50dbd82 100644 --- a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java +++ b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java @@ -354,6 +354,10 @@ public long getRetryInterval() { } // Add the request interceptor to the client builder httpClientBuilder.addInterceptorFirst(requestInterceptor); + + // Add an interceptor to add in an XSRF header + httpClientBuilder.addInterceptorLast(new XsrfHttpRequestInterceptor()); + // Configure http client for SSL if (useSsl) { String useTwoWaySSL = sessConfMap.get(JdbcConnectionParams.USE_TWO_WAY_SSL); diff --git a/jdbc/src/java/org/apache/hive/jdbc/XsrfHttpRequestInterceptor.java b/jdbc/src/java/org/apache/hive/jdbc/XsrfHttpRequestInterceptor.java new file mode 100644 index 0000000..0237fd1 --- /dev/null +++ b/jdbc/src/java/org/apache/hive/jdbc/XsrfHttpRequestInterceptor.java @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hive.jdbc; + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; + +public class XsrfHttpRequestInterceptor implements HttpRequestInterceptor { + + // Note : This implements HttpRequestInterceptor rather than extending + // HttpRequestInterceptorBase, because that class is an auth-specific + // class and refactoring would kludge too many things that are potentially + // public api. + // + // At the base, though, what we do is a very simple thing to protect + // against CSRF attacks, and that is to simply add another header. If + // HS2 is running with an XSRF filter enabled, then it will reject all + // requests that do not contain this. Thus, we add this in here on the + // client-side. This simple check prevents random other websites from + // redirecting a browser that has login credentials from making a + // request to HS2 on their behalf. + + private static boolean injectHeader = true; + + public static void enableHeaderInjection(boolean enabled){ + injectHeader = enabled; + } + + @Override + public void process(HttpRequest httpRequest, HttpContext httpContext) + throws HttpException, IOException { + if (injectHeader){ + httpRequest.addHeader("X-XSRF-HEADER", "true"); + } + } +} diff --git a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpCLIService.java b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpCLIService.java index 2d9c8e2..844baf7 100644 --- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpCLIService.java +++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpCLIService.java @@ -26,6 +26,7 @@ import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hadoop.hive.conf.HiveConf.ConfVars; import org.apache.hadoop.hive.shims.ShimLoader; +import org.apache.hadoop.hive.shims.Utils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.util.Shell; import org.apache.hive.service.auth.HiveAuthFactory; @@ -39,6 +40,7 @@ import org.apache.thrift.server.TServlet; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; +import org.eclipse.jetty.servlet.FilterMapping; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -129,6 +131,15 @@ public void run() { final ServletContextHandler context = new ServletContextHandler( ServletContextHandler.SESSIONS); context.setContextPath("/"); + if (hiveConf.getBoolean(ConfVars.HIVE_SERVER2_XSRF_FILTER_ENABLED.varname, false)){ + // context.addFilter(Utils.getXSRFFilterHolder(null, null), "/" , + // FilterMapping.REQUEST); + // Filtering does not work here currently, doing filter in ThriftHttpServlet + LOG.debug("XSRF filter enabled"); + } else { + LOG.warn("XSRF filter disabled"); + } + String httpPath = getHttpPath(hiveConf .getVar(HiveConf.ConfVars.HIVE_SERVER2_THRIFT_HTTP_PATH)); httpServer.setHandler(context); diff --git a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java index 74d73b7..b7a1e2d 100644 --- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java +++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java @@ -42,6 +42,7 @@ import org.apache.hadoop.hive.conf.HiveConf.ConfVars; import org.apache.hadoop.hive.shims.HadoopShims.KerberosNameShim; import org.apache.hadoop.hive.shims.ShimLoader; +import org.apache.hadoop.hive.shims.Utils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticator; import org.apache.hive.service.CookieSigner; @@ -128,6 +129,13 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) boolean requireNewCookie = false; try { + if (hiveConf.getBoolean(ConfVars.HIVE_SERVER2_XSRF_FILTER_ENABLED.varname,false)){ + boolean continueProcessing = Utils.doXsrfFilter(request,response,null,null); + if (!continueProcessing){ + LOG.warn("Request did not have valid XSRF header, rejecting."); + return; + } + } // If the cookie based authentication is already enabled, parse the // request and validate the request cookies. if (isCookieAuthEnabled) { diff --git a/shims/common/src/main/java/org/apache/hadoop/hive/shims/Utils.java b/shims/common/src/main/java/org/apache/hadoop/hive/shims/Utils.java index 4bcb8c3..3c93186 100644 --- a/shims/common/src/main/java/org/apache/hadoop/hive/shims/Utils.java +++ b/shims/common/src/main/java/org/apache/hadoop/hive/shims/Utils.java @@ -19,12 +19,26 @@ package org.apache.hadoop.hive.shims; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.LoginException; import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.hadoop.hive.thrift.DelegationTokenIdentifier; import org.apache.hadoop.hive.thrift.DelegationTokenSelector; @@ -39,6 +53,8 @@ public class Utils { + private static final Logger LOG = LoggerFactory.getLogger(Utils.class); + private static final boolean IBM_JAVA = System.getProperty("java.vendor") .contains("IBM"); @@ -187,5 +203,100 @@ public JaasConfiguration(String hiveLoginContextName, String principal, String k } } + public static final String XSRF_CUSTOM_HEADER_PARAM = "custom-header"; + public static final String XSRF_CUSTOM_METHODS_TO_IGNORE_PARAM = "methods-to-ignore"; + private static final String XSRF_HEADER_DEFAULT = "X-XSRF-HEADER"; + private static final Set XSRF_METHODS_TO_IGNORE_DEFAULT = new HashSet(Arrays.asList("GET", "OPTIONS", "HEAD", "TRACE")); + + /* + * Return Hadoop-native RestCsrfPreventionFilter if it is available. + * Otherwise, construct our own copy of its logic. + */ + public static Filter getXSRFFilter() { + String filterClass = "org.apache.hadoop.security.http.RestCsrfPreventionFilter"; + try { + Class klass = (Class) Class.forName(filterClass); + Filter f = klass.newInstance(); + LOG.debug("Filter {} found, using as-is.", filterClass); + return f; + } catch (Exception e) { + // ClassNotFoundException, InstantiationException, IllegalAccessException + // Class could not be init-ed, use our local copy + LOG.debug("Unable to use {}, got exception {}. Using internal shims impl of filter.", + filterClass, e.getClass().getName()); + } + return Utils.constructXSRFFilter(); + } + + private static Filter constructXSRFFilter() { + // Note Hadoop 2.7.1 onwards includes a RestCsrfPreventionFilter class that is + // usable as-is. However, since we have to work on a multitude of hadoop versions + // including very old ones, we either duplicate their code here, or not support + // an XSRFFilter on older versions of hadoop So, we duplicate to minimize evil(ugh). + // See HADOOP-12691 for details of what this is doing. + // This method should never be called if Hadoop 2.7+ is available. + + return new Filter(){ + + private String headerName = XSRF_HEADER_DEFAULT; + private Set methodsToIgnore = XSRF_METHODS_TO_IGNORE_DEFAULT; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String customHeader = filterConfig.getInitParameter(XSRF_CUSTOM_HEADER_PARAM); + if (customHeader != null) { + headerName = customHeader; + } + String customMethodsToIgnore = filterConfig.getInitParameter( + XSRF_CUSTOM_METHODS_TO_IGNORE_PARAM); + if (customMethodsToIgnore != null) { + parseMethodsToIgnore(customMethodsToIgnore); + } + } + + void parseMethodsToIgnore(String mti) { + String[] methods = mti.split(","); + methodsToIgnore = new HashSet(); + for (int i = 0; i < methods.length; i++) { + methodsToIgnore.add(methods[i]); + } + } + + @Override + public void doFilter( + ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (doXsrfFilter(request, response, methodsToIgnore, headerName)){ + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + // do nothing + } + }; + } + + // Method that provides similar filter functionality to filter-holder above, useful when + // calling from code that does not use filters as-is. + public static boolean doXsrfFilter(ServletRequest request, ServletResponse response, + Set methodsToIgnore, String headerName) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest)request; + if (methodsToIgnore == null) { methodsToIgnore = XSRF_METHODS_TO_IGNORE_DEFAULT ; } + if (headerName == null ) { headerName = XSRF_HEADER_DEFAULT; } + if (methodsToIgnore.contains(httpRequest.getMethod()) || + httpRequest.getHeader(headerName) != null) { + return true; + } else { + ((HttpServletResponse)response).sendError( + HttpServletResponse.SC_BAD_REQUEST, + "Missing Required Header for Vulnerability Protection"); + response.getWriter().println( + "XSRF filter denial, requests must contain header : " + headerName); + return false; + } + } + }