HADOOP-13352. Make X-FRAME-OPTIONS configurable in HttpServer2. Contributed by Anu Engineer.
This commit is contained in:
parent
2e2caeb50e
commit
324923e1bc
|
@ -137,6 +137,11 @@ public final class HttpServer2 implements FilterContainer {
|
|||
static final String STATE_DESCRIPTION_ALIVE = " - alive";
|
||||
static final String STATE_DESCRIPTION_NOT_LIVE = " - not live";
|
||||
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.
|
||||
|
@ -169,6 +174,9 @@ public final class HttpServer2 implements FilterContainer {
|
|||
private String authFilterConfigurationPrefix = "hadoop.http.authentication.";
|
||||
private String excludeCiphers;
|
||||
|
||||
private boolean xFrameEnabled;
|
||||
private XFrameOption xFrameOption = XFrameOption.SAMEORIGIN;
|
||||
|
||||
public Builder setName(String name){
|
||||
this.name = name;
|
||||
return this;
|
||||
|
@ -277,6 +285,30 @@ public final class HttpServer2 implements FilterContainer {
|
|||
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 {
|
||||
Preconditions.checkNotNull(name, "name is not set");
|
||||
Preconditions.checkState(!endpoints.isEmpty(), "No endpoints specified");
|
||||
|
@ -343,6 +375,9 @@ public final class HttpServer2 implements FilterContainer {
|
|||
this.webServer = new Server();
|
||||
this.adminsAcl = b.adminsAcl;
|
||||
this.webAppContext = createWebAppContext(b.name, b.conf, adminsAcl, appDir);
|
||||
this.xFrameOptionIsEnabled = b.xFrameEnabled;
|
||||
this.xFrameOption = b.xFrameOption;
|
||||
|
||||
try {
|
||||
this.secretProvider =
|
||||
constructSecretProvider(b, webAppContext.getServletContext());
|
||||
|
@ -399,7 +434,11 @@ public final class HttpServer2 implements FilterContainer {
|
|||
|
||||
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);
|
||||
if (initializers != null) {
|
||||
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.
|
||||
*/
|
||||
public static class QuotingInputFilter implements Filter {
|
||||
private static final XFrameOption X_FRAME_OPTION = XFrameOption.SAMEORIGIN;
|
||||
|
||||
private FilterConfig config;
|
||||
|
||||
public static class RequestQuoter extends HttpServletRequestWrapper {
|
||||
|
@ -1271,7 +1310,11 @@ public final class HttpServer2 implements FilterContainer {
|
|||
} else if (mime.startsWith("application/xml")) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1306,5 +1349,23 @@ public final class HttpServer2 implements FilterContainer {
|
|||
public String toString() {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,6 +169,25 @@ public class HttpServerFunctionalTest extends Assert {
|
|||
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)
|
||||
throws IOException {
|
||||
return localServerBuilder(webapp).setFindPort(true).setConf(conf).setACL(adminsAcl).build();
|
||||
|
|
|
@ -31,7 +31,9 @@ import org.apache.hadoop.security.authorize.AccessControlList;
|
|||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.internal.util.reflection.Whitebox;
|
||||
import org.mortbay.jetty.Connector;
|
||||
|
@ -69,6 +71,9 @@ public class TestHttpServer extends HttpServerFunctionalTest {
|
|||
private static HttpServer2 server;
|
||||
private static final int MAX_THREADS = 10;
|
||||
|
||||
@Rule
|
||||
public ExpectedException exception = ExpectedException.none();
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class EchoMapServlet extends HttpServlet {
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@ -236,15 +241,70 @@ public class TestHttpServer extends HttpServerFunctionalTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResonseContainsXFrameOptions() throws IOException {
|
||||
URL url = new URL(baseUrl, "");
|
||||
public void testHttpResonseContainsXFrameOptions() throws Exception {
|
||||
validateXFrameOption(HttpServer2.XFrameOption.SAMEORIGIN);
|
||||
}
|
||||
|
||||
@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");
|
||||
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();
|
||||
|
||||
String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS");
|
||||
assertTrue("X-FRAME-OPTIONS is absent in the header", xfoHeader != null);
|
||||
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
|
||||
* from the request parameter user.name. Wraps around the request so that
|
||||
|
|
Loading…
Reference in New Issue