diff --git a/hbase-common/src/main/resources/hbase-default.xml b/hbase-common/src/main/resources/hbase-default.xml
index e446a24..b7e4e21 100644
--- a/hbase-common/src/main/resources/hbase-default.xml
+++ b/hbase-common/src/main/resources/hbase-default.xml
@@ -1716,6 +1716,24 @@ possible configurations would overwhelm and obscure the important.
The max number of threads used in MobCompactor.
+
+ hbase.rest-csrf.browser-useragents-regex
+ ^Mozilla.*,^Opera.*
+
+ A comma-separated list of regular expressions used to match against an HTTP
+ request's User-Agent header when protection against cross-site request
+ forgery (CSRF) is enabled for REST server by setting
+ hbase.rest.csrf.enabled to true. If the incoming User-Agent matches
+ any of these regular expressions, then the request is considered to be sent
+ by a browser, and therefore CSRF prevention is enforced. If the request's
+ User-Agent does not match any of these regular expressions, then the request
+ is considered to be sent by something other than a browser, such as scripted
+ automation. In this case, CSRF is not a potential attack vector, so
+ the prevention is not enforced. This helps achieve backwards-compatibility
+ with existing automation that has not been updated to send the CSRF
+ prevention header.
+
+
hbase.snapshot.master.timeout.millis
300000
diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java
index 0460b0a..4011421 100644
--- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java
+++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java
@@ -18,10 +18,13 @@
package org.apache.hadoop.hbase.rest;
+import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Set;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
@@ -35,6 +38,7 @@ import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
+import org.apache.hadoop.hbase.http.HttpServer;
import org.apache.hadoop.hbase.http.InfoServer;
import org.apache.hadoop.hbase.rest.filter.AuthFilter;
import org.apache.hadoop.hbase.security.UserProvider;
@@ -42,6 +46,7 @@ import org.apache.hadoop.hbase.util.DNS;
import org.apache.hadoop.hbase.util.HttpServerUtil;
import org.apache.hadoop.hbase.util.Strings;
import org.apache.hadoop.hbase.util.VersionInfo;
+import org.apache.hadoop.util.StringUtils;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.nio.SelectChannelConnector;
@@ -67,6 +72,14 @@ import com.sun.jersey.spi.container.servlet.ServletContainer;
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
public class RESTServer implements Constants {
+ static String REST_CSRF_ENABLED_KEY = "hbase.rest.csrf.enabled";
+ static boolean REST_CSRF_ENABLED_DEFAULT = false;
+ static boolean restCSRFEnabled = false;
+ static String REST_CSRF_CUSTOM_HEADER_KEY ="hbase.rest.csrf.custom.header";
+ static String REST_CSRF_CUSTOM_HEADER_DEFAULT = "X-XSRF-HEADER";
+ static String REST_CSRF_METHODS_TO_IGNORE_KEY = "hbase.rest.csrf.methods.to.ignore";
+ static String REST_CSRF_METHODS_TO_IGNORE_DEFAULT = "GET,OPTIONS,HEAD,TRACE";
+
private static void printUsageAndExit(Options options, int exitCode) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("bin/hbase rest start", "", options,
@@ -76,6 +89,48 @@ public class RESTServer implements Constants {
}
/**
+ * Returns a list of strings from a comma-delimited configuration value.
+ *
+ * @param conf configuration to check
+ * @param name configuration property name
+ * @param defaultValue default value if no value found for name
+ * @return list of strings from comma-delimited configuration value, or an
+ * empty list if not found
+ */
+ private static List getTrimmedStringList(Configuration conf,
+ String name, String defaultValue) {
+ String valueString = conf.get(name, defaultValue);
+ if (valueString == null) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(StringUtils.getTrimmedStringCollection(valueString));
+ }
+
+ static String REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY = "hbase.rest-csrf.browser-useragents-regex";
+ static boolean addCSRFFilter(Context context, Configuration conf, Log LOG) {
+ restCSRFEnabled = conf.getBoolean(REST_CSRF_ENABLED_KEY, REST_CSRF_ENABLED_DEFAULT);
+ if (restCSRFEnabled) {
+ String[] urls = { "/*" };
+ Set restCsrfMethodsToIgnore = new HashSet<>();
+ restCsrfMethodsToIgnore.addAll(getTrimmedStringList(conf,
+ REST_CSRF_METHODS_TO_IGNORE_KEY, REST_CSRF_METHODS_TO_IGNORE_DEFAULT));
+ String clsName = "org.apache.hadoop.security.http.RestCsrfPreventionFilter";
+ try {
+ Class clazz = Class.forName(clsName);
+ Method m = clazz.getMethod("getFilterParams", Configuration.class, String.class);
+ Map restCsrfParams = (Map) m.invoke(null,
+ conf, "hbase.rest-csrf.");
+ HttpServer.defineFilter(context, "csrf", clsName,
+ restCsrfParams, urls);
+ } catch (Exception e) {
+ LOG.info("Unable to add Filter: ", e);
+ restCSRFEnabled = false;
+ }
+ }
+ return restCSRFEnabled;
+ }
+
+ /**
* The main method for the HBase rest server.
* @param args command-line arguments
* @throws Exception exception
@@ -234,6 +289,7 @@ public class RESTServer implements Constants {
filter = filter.trim();
context.addFilter(Class.forName(filter), "/*", 0);
}
+ addCSRFFilter(context, conf, LOG);
HttpServerUtil.constrainHttpMethods(context);
// Put up info server.
diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java
index ebedf57..142c276 100644
--- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java
+++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java
@@ -375,13 +375,27 @@ public class Client {
/**
* Send a PUT request
- * @param cluster the cluster definition
* @param path the path or URI
* @param contentType the content MIME type
* @param content the content bytes
+ * @param extraHdr extra Header to send
* @return a Response object with response detail
* @throws IOException
*/
+ public Response put(String path, String contentType, byte[] content, Header extraHdr)
+ throws IOException {
+ return put(cluster, path, contentType, content, extraHdr);
+ }
+
+ /**
+ * Send a PUT request
+ * @param cluster the cluster definition
+ * @param path the path or URI
+ * @param contentType the content MIME type
+ * @param content the content bytes
+ * @return a Response object with response detail
+ * @throws IOException for error
+ */
public Response put(Cluster cluster, String path, String contentType,
byte[] content) throws IOException {
Header[] headers = new Header[1];
@@ -391,6 +405,27 @@ public class Client {
/**
* Send a PUT request
+ * @param cluster the cluster definition
+ * @param path the path or URI
+ * @param contentType the content MIME type
+ * @param content the content bytes
+ * @param extraHdr additional Header to send
+ * @return a Response object with response detail
+ * @throws IOException for error
+ */
+ public Response put(Cluster cluster, String path, String contentType,
+ byte[] content, Header extraHdr) throws IOException {
+ int cnt = extraHdr == null ? 1 : 2;
+ Header[] headers = new Header[cnt];
+ headers[0] = new Header("Content-Type", contentType);
+ if (extraHdr != null) {
+ headers[1] = extraHdr;
+ }
+ return put(cluster, path, headers, content);
+ }
+
+ /**
+ * Send a PUT request
* @param path the path or URI
* @param headers the HTTP headers to include, Content-Type must be
* supplied
@@ -442,13 +477,27 @@ public class Client {
/**
* Send a POST request
- * @param cluster the cluster definition
* @param path the path or URI
* @param contentType the content MIME type
* @param content the content bytes
+ * @param extraHdr additional Header to send
* @return a Response object with response detail
* @throws IOException
*/
+ public Response post(String path, String contentType, byte[] content, Header extraHdr)
+ throws IOException {
+ return post(cluster, path, contentType, content, extraHdr);
+ }
+
+ /**
+ * Send a POST request
+ * @param cluster the cluster definition
+ * @param path the path or URI
+ * @param contentType the content MIME type
+ * @param content the content bytes
+ * @return a Response object with response detail
+ * @throws IOException for error
+ */
public Response post(Cluster cluster, String path, String contentType,
byte[] content) throws IOException {
Header[] headers = new Header[1];
@@ -458,6 +507,27 @@ public class Client {
/**
* Send a POST request
+ * @param cluster the cluster definition
+ * @param path the path or URI
+ * @param contentType the content MIME type
+ * @param content the content bytes
+ * @param extraHdr additional Header to send
+ * @return a Response object with response detail
+ * @throws IOException for error
+ */
+ public Response post(Cluster cluster, String path, String contentType,
+ byte[] content, Header extraHdr) throws IOException {
+ int cnt = extraHdr == null ? 1 : 2;
+ Header[] headers = new Header[cnt];
+ headers[0] = new Header("Content-Type", contentType);
+ if (extraHdr != null) {
+ headers[1] = extraHdr;
+ }
+ return post(cluster, path, headers, content);
+ }
+
+ /**
+ * Send a POST request
* @param path the path or URI
* @param headers the HTTP headers to include, Content-Type must be
* supplied
@@ -506,11 +576,22 @@ public class Client {
/**
* Send a DELETE request
- * @param cluster the cluster definition
* @param path the path or URI
+ * @param extraHdr additional Header to send
* @return a Response object with response detail
* @throws IOException
*/
+ public Response delete(String path, Header extraHdr) throws IOException {
+ return delete(cluster, path, extraHdr);
+ }
+
+ /**
+ * Send a DELETE request
+ * @param cluster the cluster definition
+ * @param path the path or URI
+ * @return a Response object with response detail
+ * @throws IOException for error
+ */
public Response delete(Cluster cluster, String path) throws IOException {
DeleteMethod method = new DeleteMethod();
try {
@@ -522,4 +603,24 @@ public class Client {
method.releaseConnection();
}
}
+
+ /**
+ * Send a DELETE request
+ * @param cluster the cluster definition
+ * @param path the path or URI
+ * @return a Response object with response detail
+ * @throws IOException for error
+ */
+ public Response delete(Cluster cluster, String path, Header extraHdr) throws IOException {
+ DeleteMethod method = new DeleteMethod();
+ try {
+ Header[] headers = { extraHdr };
+ int code = execute(cluster, method, headers, path);
+ headers = method.getResponseHeaders();
+ byte[] content = method.getResponseBody();
+ return new Response(code, headers, content);
+ } finally {
+ method.releaseConnection();
+ }
+ }
}
diff --git a/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/HBaseRESTTestingUtility.java b/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/HBaseRESTTestingUtility.java
index 628b17c..25e3254 100644
--- a/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/HBaseRESTTestingUtility.java
+++ b/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/HBaseRESTTestingUtility.java
@@ -42,10 +42,10 @@ public class HBaseRESTTestingUtility {
return testServletPort;
}
- public void startServletContainer(Configuration conf) throws Exception {
+ public boolean startServletContainer(Configuration conf) throws Exception {
if (server != null) {
LOG.error("ServletContainer already running");
- return;
+ return false;
}
// Inject the conf for the test by being first to make singleton
@@ -75,6 +75,8 @@ public class HBaseRESTTestingUtility {
filter = filter.trim();
context.addFilter(Class.forName(filter), "/*", 0);
}
+ conf.set(RESTServer.REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY, ".*");
+ boolean ret = RESTServer.addCSRFFilter(context, conf, LOG);
HttpServerUtil.constrainHttpMethods(context);
LOG.info("Loaded filter classes :" + filterClasses);
// start the server
@@ -84,6 +86,7 @@ public class HBaseRESTTestingUtility {
LOG.info("started " + server.getClass().getName() + " on port " +
testServletPort);
+ return ret;
}
public void shutdownServletContainer() {
diff --git a/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestMultiRowResource.java b/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestMultiRowResource.java
index c7da65a..f70b283 100644
--- a/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestMultiRowResource.java
+++ b/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestMultiRowResource.java
@@ -18,6 +18,7 @@
*/
package org.apache.hadoop.hbase.rest;
+import org.apache.commons.httpclient.Header;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.Admin;
@@ -36,6 +37,8 @@ import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.JAXBContext;
@@ -43,10 +46,15 @@ import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
import static org.junit.Assert.assertEquals;
+
@Category({RestTests.class, MediumTests.class})
+@RunWith(Parameterized.class)
public class TestMultiRowResource {
private static final TableName TABLE = TableName.valueOf("TestRowResource");
@@ -69,12 +77,29 @@ public class TestMultiRowResource {
private static Unmarshaller unmarshaller;
private static Configuration conf;
+ private static Header extraHdr = null;
+ private static boolean csrfEnabled = true;
+
+ @Parameterized.Parameters
+ public static Collection