HADOOP-13352. Make X-FRAME-OPTIONS configurable in HttpServer2. Contributed by Anu Engineer.

This commit is contained in:
Jitendra Pandey 2016-07-08 14:17:14 -07:00
parent 2e2caeb50e
commit 324923e1bc
3 changed files with 148 additions and 8 deletions

View File

@ -137,6 +137,11 @@ public final class HttpServer2 implements FilterContainer {
static final String STATE_DESCRIPTION_ALIVE = " - alive"; static final String STATE_DESCRIPTION_ALIVE = " - alive";
static final String STATE_DESCRIPTION_NOT_LIVE = " - not live"; static final String STATE_DESCRIPTION_NOT_LIVE = " - not live";
private final SignerSecretProvider secretProvider; 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";
/** /**
* Class to construct instances of HTTP server with specific options. * Class to construct instances of HTTP server with specific options.
@ -169,6 +174,9 @@ public final class HttpServer2 implements FilterContainer {
private String authFilterConfigurationPrefix = "hadoop.http.authentication."; private String authFilterConfigurationPrefix = "hadoop.http.authentication.";
private String excludeCiphers; private String excludeCiphers;
private boolean xFrameEnabled;
private XFrameOption xFrameOption = XFrameOption.SAMEORIGIN;
public Builder setName(String name){ public Builder setName(String name){
this.name = name; this.name = name;
return this; return this;
@ -277,6 +285,30 @@ public final class HttpServer2 implements FilterContainer {
return this; return this;
} }
/**
* Adds the ability to control X_FRAME_OPTIONS on HttpServer2.
* @param xFrameEnabled - True enables X_FRAME_OPTIONS false disables it.
* @return Builder.
*/
public Builder configureXFrame(boolean xFrameEnabled) {
this.xFrameEnabled = xFrameEnabled;
return this;
}
/**
* Sets a valid X-Frame-option that can be used by HttpServer2.
* @param option - String DENY, SAMEORIGIN or ALLOW-FROM are the only valid
* options. Any other value will throw IllegalArgument
* Exception.
* @return Builder.
*/
public Builder setXFrameOption(String option) {
this.xFrameOption = XFrameOption.getEnum(option);
return this;
}
public HttpServer2 build() throws IOException { public HttpServer2 build() throws IOException {
Preconditions.checkNotNull(name, "name is not set"); Preconditions.checkNotNull(name, "name is not set");
Preconditions.checkState(!endpoints.isEmpty(), "No endpoints specified"); Preconditions.checkState(!endpoints.isEmpty(), "No endpoints specified");
@ -343,6 +375,9 @@ public final class HttpServer2 implements FilterContainer {
this.webServer = new Server(); this.webServer = new Server();
this.adminsAcl = b.adminsAcl; this.adminsAcl = b.adminsAcl;
this.webAppContext = createWebAppContext(b.name, b.conf, adminsAcl, appDir); this.webAppContext = createWebAppContext(b.name, b.conf, adminsAcl, appDir);
this.xFrameOptionIsEnabled = b.xFrameEnabled;
this.xFrameOption = b.xFrameOption;
try { try {
this.secretProvider = this.secretProvider =
constructSecretProvider(b, webAppContext.getServletContext()); constructSecretProvider(b, webAppContext.getServletContext());
@ -399,7 +434,11 @@ public final class HttpServer2 implements FilterContainer {
addDefaultApps(contexts, appDir, conf); addDefaultApps(contexts, appDir, conf);
addGlobalFilter("safety", QuotingInputFilter.class.getName(), null); Map<String, String> xFrameParams = new HashMap<>();
xFrameParams.put(X_FRAME_ENABLED,
String.valueOf(this.xFrameOptionIsEnabled));
xFrameParams.put(X_FRAME_VALUE, this.xFrameOption.toString());
addGlobalFilter("safety", QuotingInputFilter.class.getName(), xFrameParams);
final FilterInitializer[] initializers = getFilterInitializers(conf); final FilterInitializer[] initializers = getFilterInitializers(conf);
if (initializers != null) { if (initializers != null) {
conf = new Configuration(conf); conf = new Configuration(conf);
@ -1151,7 +1190,7 @@ public final class HttpServer2 implements FilterContainer {
* sets X-FRAME-OPTIONS in the header to mitigate clickjacking attacks. * sets X-FRAME-OPTIONS in the header to mitigate clickjacking attacks.
*/ */
public static class QuotingInputFilter implements Filter { public static class QuotingInputFilter implements Filter {
private static final XFrameOption X_FRAME_OPTION = XFrameOption.SAMEORIGIN;
private FilterConfig config; private FilterConfig config;
public static class RequestQuoter extends HttpServletRequestWrapper { public static class RequestQuoter extends HttpServletRequestWrapper {
@ -1271,7 +1310,11 @@ public final class HttpServer2 implements FilterContainer {
} else if (mime.startsWith("application/xml")) { } else if (mime.startsWith("application/xml")) {
httpResponse.setContentType("text/xml; charset=utf-8"); httpResponse.setContentType("text/xml; charset=utf-8");
} }
httpResponse.addHeader("X-FRAME-OPTIONS", X_FRAME_OPTION.toString());
if(Boolean.valueOf(this.config.getInitParameter(X_FRAME_ENABLED))) {
httpResponse.addHeader("X-FRAME-OPTIONS",
this.config.getInitParameter(X_FRAME_VALUE));
}
chain.doFilter(quoted, httpResponse); chain.doFilter(quoted, httpResponse);
} }
@ -1306,5 +1349,23 @@ public final class HttpServer2 implements FilterContainer {
public String toString() { public String toString() {
return this.name; return this.name;
} }
/**
* We cannot use valueOf since the AllowFrom enum differs from its value
* Allow-From. This is a helper method that does exactly what valueof does,
* but allows us to handle the AllowFrom issue gracefully.
*
* @param value - String must be DENY, SAMEORIGIN or ALLOW-FROM.
* @return XFrameOption or throws IllegalException.
*/
private static XFrameOption getEnum(String value) {
Preconditions.checkState(value != null && !value.isEmpty());
for (XFrameOption xoption : values()) {
if (value.equals(xoption.toString())) {
return xoption;
}
}
throw new IllegalArgumentException("Unexpected value in xFrameOption.");
}
} }
} }

View File

@ -169,6 +169,25 @@ public class HttpServerFunctionalTest extends Assert {
return localServerBuilder(webapp).setFindPort(true).setConf(conf).build(); return localServerBuilder(webapp).setFindPort(true).setConf(conf).build();
} }
/**
* Create a test server with xFrame options enabled.
* @param xFrameEnabled - true to enable xFrameSupport
* @param xFrameOptionValue - Option Value
* @param conf the configuration to use for the server
* @return
* @throws IOException
*/
public static HttpServer2 createServer(boolean xFrameEnabled,
String xFrameOptionValue,
Configuration conf)
throws IOException {
return localServerBuilder(TEST).setFindPort(true)
.configureXFrame(xFrameEnabled)
.setXFrameOption(xFrameOptionValue)
.setConf(conf)
.build();
}
public static HttpServer2 createServer(String webapp, Configuration conf, AccessControlList adminsAcl) public static HttpServer2 createServer(String webapp, Configuration conf, AccessControlList adminsAcl)
throws IOException { throws IOException {
return localServerBuilder(webapp).setFindPort(true).setConf(conf).setACL(adminsAcl).build(); return localServerBuilder(webapp).setFindPort(true).setConf(conf).setACL(adminsAcl).build();

View File

@ -31,7 +31,9 @@ import org.apache.hadoop.security.authorize.AccessControlList;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.internal.util.reflection.Whitebox; import org.mockito.internal.util.reflection.Whitebox;
import org.mortbay.jetty.Connector; import org.mortbay.jetty.Connector;
@ -69,6 +71,9 @@ public class TestHttpServer extends HttpServerFunctionalTest {
private static HttpServer2 server; private static HttpServer2 server;
private static final int MAX_THREADS = 10; private static final int MAX_THREADS = 10;
@Rule
public ExpectedException exception = ExpectedException.none();
@SuppressWarnings("serial") @SuppressWarnings("serial")
public static class EchoMapServlet extends HttpServlet { public static class EchoMapServlet extends HttpServlet {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -236,14 +241,69 @@ public class TestHttpServer extends HttpServerFunctionalTest {
} }
@Test @Test
public void testHttpResonseContainsXFrameOptions() throws IOException { public void testHttpResonseContainsXFrameOptions() throws Exception {
URL url = new URL(baseUrl, ""); validateXFrameOption(HttpServer2.XFrameOption.SAMEORIGIN);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); }
conn.connect();
@Test
public void testHttpResonseContainsDeny() throws Exception {
validateXFrameOption(HttpServer2.XFrameOption.DENY);
}
@Test
public void testHttpResonseContainsAllowFrom() throws Exception {
validateXFrameOption(HttpServer2.XFrameOption.ALLOWFROM);
}
private void validateXFrameOption(HttpServer2.XFrameOption option) throws
Exception {
Configuration conf = new Configuration();
boolean xFrameEnabled = true;
HttpServer2 httpServer = createServer(xFrameEnabled,
option.toString(), conf);
try {
HttpURLConnection conn = getHttpURLConnection(httpServer);
String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS"); String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS");
assertTrue("X-FRAME-OPTIONS is absent in the header", xfoHeader != null); assertTrue("X-FRAME-OPTIONS is absent in the header", xfoHeader != null);
assertTrue(xfoHeader.endsWith(option.toString()));
} finally {
httpServer.stop();
} }
}
@Test
public void testHttpResonseDoesNotContainXFrameOptions() throws Exception {
Configuration conf = new Configuration();
boolean xFrameEnabled = false;
HttpServer2 httpServer = createServer(xFrameEnabled,
HttpServer2.XFrameOption.SAMEORIGIN.toString(), conf);
try {
HttpURLConnection conn = getHttpURLConnection(httpServer);
String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS");
assertTrue("Unexpected X-FRAME-OPTIONS in header", xfoHeader == null);
} finally {
httpServer.stop();
}
}
private HttpURLConnection getHttpURLConnection(HttpServer2 httpServer)
throws IOException {
httpServer.start();
URL newURL = getServerURL(httpServer);
URL url = new URL(newURL, "");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.connect();
return conn;
}
@Test
public void testHttpResonseInvalidValueType() throws Exception {
Configuration conf = new Configuration();
boolean xFrameEnabled = true;
exception.expect(IllegalArgumentException.class);
createServer(xFrameEnabled, "Hadoop", conf);
}
/** /**
* Dummy filter that mimics as an authentication filter. Obtains user identity * Dummy filter that mimics as an authentication filter. Obtains user identity