http://git-wip-us.apache.org/repos/asf/knox/blob/7953de69/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/deploy/WebAppSecContributor.java ---------------------------------------------------------------------- diff --git a/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/deploy/WebAppSecContributor.java b/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/deploy/WebAppSecContributor.java index eed8ec3..ed5e98d 100644 --- a/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/deploy/WebAppSecContributor.java +++ b/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/deploy/WebAppSecContributor.java @@ -29,8 +29,7 @@ import org.apache.knox.gateway.descriptor.ResourceDescriptor; import org.apache.knox.gateway.topology.Provider; import org.apache.knox.gateway.topology.Service; -public class WebAppSecContributor extends - ProviderDeploymentContributorBase { +public class WebAppSecContributor extends ProviderDeploymentContributorBase { private static final String ROLE = "webappsec"; private static final String NAME = "WebAppSec"; private static final String CSRF_SUFFIX = "_CSRF"; @@ -42,6 +41,9 @@ public class WebAppSecContributor extends private static final String XFRAME_OPTIONS_SUFFIX = "_XFRAMEOPTIONS"; private static final String XFRAME_OPTIONS_FILTER_CLASSNAME = "org.apache.knox.gateway.webappsec.filter.XFrameOptionsFilter"; private static final String XFRAME_OPTIONS_ENABLED = "xframe.options.enabled"; + private static final String XCONTENT_TYPE_OPTIONS_SUFFIX = "_XCONTENTTYPEOPTIONS"; + private static final String XCONTENT_TYPE_OPTIONS_FILTER_CLASSNAME = "org.apache.knox.gateway.webappsec.filter.XContentTypeOptionsFilter"; + private static final String XCONTENT_TYPE_OPTIONS_ENABLED = "xcontent-type.options.enabled"; private static final String XSS_PROTECTION_SUFFIX = "_XSSPROTECTION"; private static final String XSS_PROTECTION_FILTER_CLASSNAME = "org.apache.knox.gateway.webappsec.filter.XSSProtectionFilter"; private static final String XSS_PROTECTION_ENABLED = "xss.protection.enabled"; @@ -66,54 +68,83 @@ public class WebAppSecContributor extends } @Override - public void contributeFilter(DeploymentContext context, Provider provider, Service service, - ResourceDescriptor resource, List<FilterParamDescriptor> params) { + public void contributeFilter(DeploymentContext context, + Provider provider, + Service service, + ResourceDescriptor resource, + List<FilterParamDescriptor> params) { Provider webappsec = context.getTopology().getProvider(ROLE, NAME); if (webappsec != null && webappsec.isEnabled()) { Map<String,String> map = provider.getParams(); if (params == null) { - params = new ArrayList<FilterParamDescriptor>(); + params = new ArrayList<>(); } Map<String, String> providerParams = provider.getParams(); // CORS support String corsEnabled = map.get(CORS_ENABLED); - if ( corsEnabled != null && "true".equals(corsEnabled)) { + if (corsEnabled != null && "true".equals(corsEnabled)) { provisionConfig(resource, providerParams, params, "cors."); - resource.addFilter().name( getName() + CORS_SUFFIX ).role( getRole() ).impl( CORS_FILTER_CLASSNAME ).params( params ); + resource.addFilter().name(getName() + CORS_SUFFIX) + .role(getRole()) + .impl(CORS_FILTER_CLASSNAME) + .params(params); } // CRSF - params = new ArrayList<FilterParamDescriptor>(); + params = new ArrayList<>(); String csrfEnabled = map.get(CSRF_ENABLED); - if ( csrfEnabled != null && "true".equals(csrfEnabled)) { + if (csrfEnabled != null && "true".equals(csrfEnabled)) { provisionConfig(resource, providerParams, params, "csrf."); - resource.addFilter().name( getName() + CSRF_SUFFIX ).role( getRole() ).impl( CSRF_FILTER_CLASSNAME ).params( params ); + resource.addFilter().name(getName() + CSRF_SUFFIX) + .role(getRole()) + .impl(CSRF_FILTER_CLASSNAME) + .params(params); } // X-Frame-Options - clickjacking protection - params = new ArrayList<FilterParamDescriptor>(); + params = new ArrayList<>(); String xframeOptionsEnabled = map.get(XFRAME_OPTIONS_ENABLED); - if ( xframeOptionsEnabled != null && "true".equals(xframeOptionsEnabled)) { + if (xframeOptionsEnabled != null && "true".equals(xframeOptionsEnabled)) { provisionConfig(resource, providerParams, params, "xframe."); - resource.addFilter().name( getName() + XFRAME_OPTIONS_SUFFIX ).role( getRole() ).impl( XFRAME_OPTIONS_FILTER_CLASSNAME ).params( params ); + resource.addFilter().name(getName() + XFRAME_OPTIONS_SUFFIX) + .role(getRole()) + .impl(XFRAME_OPTIONS_FILTER_CLASSNAME) + .params(params); + } + + // X-Content-Type-Options - MIME type sniffing protection + params = new ArrayList<>(); + String xContentTypeOptionsEnabled = map.get(XCONTENT_TYPE_OPTIONS_ENABLED); + if (xContentTypeOptionsEnabled != null && "true".equals(xContentTypeOptionsEnabled)) { + provisionConfig(resource, providerParams, params, "xcontent-type."); + resource.addFilter().name(getName() + XCONTENT_TYPE_OPTIONS_SUFFIX) + .role(getRole()) + .impl(XCONTENT_TYPE_OPTIONS_FILTER_CLASSNAME) + .params(params); } // X-XSS-Protection - browser xss protection - params = new ArrayList<FilterParamDescriptor>(); + params = new ArrayList<>(); String xssProtectionEnabled = map.get(XSS_PROTECTION_ENABLED); - if ( xssProtectionEnabled != null && "true".equals(xssProtectionEnabled)) { + if (xssProtectionEnabled != null && "true".equals(xssProtectionEnabled)) { provisionConfig(resource, providerParams, params, "xss."); - resource.addFilter().name( getName() + XSS_PROTECTION_SUFFIX ).role( getRole() ).impl( XSS_PROTECTION_FILTER_CLASSNAME ).params( params ); + resource.addFilter().name(getName() + XSS_PROTECTION_SUFFIX) + .role(getRole()) + .impl(XSS_PROTECTION_FILTER_CLASSNAME) + .params(params); } // HTTP Strict-Transport-Security - params = new ArrayList<FilterParamDescriptor>(); + params = new ArrayList<>(); String strictTranportEnabled = map.get(STRICT_TRANSPORT_ENABLED); - if ( strictTranportEnabled != null && "true".equals(strictTranportEnabled)) { + if (strictTranportEnabled != null && "true".equals(strictTranportEnabled)) { provisionConfig(resource, providerParams, params, "strict."); - resource.addFilter().name( getName() + STRICT_TRANSPORT_SUFFIX).role( getRole() ).impl(STRICT_TRANSPORT_FILTER_CLASSNAME).params( params ); + resource.addFilter().name(getName() + STRICT_TRANSPORT_SUFFIX) + .role(getRole()) + .impl(STRICT_TRANSPORT_FILTER_CLASSNAME) + .params(params); } } } @@ -122,7 +153,7 @@ public class WebAppSecContributor extends List<FilterParamDescriptor> params, String prefix) { for(Entry<String, String> entry : providerParams.entrySet()) { if (entry.getKey().startsWith(prefix)) { - params.add( resource.createFilterParam().name( entry.getKey().toLowerCase() ).value( entry.getValue() ) ); + params.add(resource.createFilterParam().name(entry.getKey().toLowerCase()).value(entry.getValue())); } } }
http://git-wip-us.apache.org/repos/asf/knox/blob/7953de69/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/XContentTypeOptionsFilter.java ---------------------------------------------------------------------- diff --git a/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/XContentTypeOptionsFilter.java b/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/XContentTypeOptionsFilter.java new file mode 100644 index 0000000..b05b335 --- /dev/null +++ b/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/XContentTypeOptionsFilter.java @@ -0,0 +1,123 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.knox.gateway.webappsec.filter; + +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.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class XContentTypeOptionsFilter implements Filter { + + public static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options"; + + public static final String CUSTOM_HEADER_PARAM = "xcontent-type.options"; + + public static final String DEFAULT_OPTION_VALUE = "nosniff"; + + private String option = DEFAULT_OPTION_VALUE; + + + @Override + public void init(FilterConfig config) throws ServletException { + String customOption = config.getInitParameter(CUSTOM_HEADER_PARAM); + if (customOption != null) { + option = customOption; + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + ((HttpServletResponse) response).setHeader(X_CONTENT_TYPE_OPTIONS_HEADER, option); + chain.doFilter(request, new XContentTypeOptionsResponseWrapper((HttpServletResponse) response)); + } + + @Override + public void destroy() { + } + + + class XContentTypeOptionsResponseWrapper extends HttpServletResponseWrapper { + + XContentTypeOptionsResponseWrapper(HttpServletResponse res) { + super(res); + } + + @Override + public void addHeader(String name, String value) { + // don't allow additional values to be added to + // the configured options value in topology + if (!name.equals(X_CONTENT_TYPE_OPTIONS_HEADER)) { + super.addHeader(name, value); + } + } + + @Override + public void setHeader(String name, String value) { + // don't allow overwriting of configured value + if (!name.equals(X_CONTENT_TYPE_OPTIONS_HEADER)) { + super.setHeader(name, value); + } + } + + @Override + public String getHeader(String name) { + String headerValue = null; + if (name.equals(X_CONTENT_TYPE_OPTIONS_HEADER)) { + headerValue = option; + } + else { + headerValue = super.getHeader(name); + } + return headerValue; + } + + /** + * get the Header names + */ + @Override + public Collection<String> getHeaderNames() { + List<String> names = (List<String>) super.getHeaderNames(); + if (names == null) { + names = new ArrayList<>(); + } + names.add(X_CONTENT_TYPE_OPTIONS_HEADER); + return names; + } + + @Override + public Collection<String> getHeaders(String name) { + List<String> values = (List<String>) super.getHeaders(name); + if (name.equals(X_CONTENT_TYPE_OPTIONS_HEADER)) { + if (values == null) { + values = new ArrayList<>(); + } + values.add(option); + } + return values; + } + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/7953de69/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/XContentTypeOptionsFilterTest.java ---------------------------------------------------------------------- diff --git a/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/XContentTypeOptionsFilterTest.java b/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/XContentTypeOptionsFilterTest.java new file mode 100644 index 0000000..e5fb587 --- /dev/null +++ b/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/XContentTypeOptionsFilterTest.java @@ -0,0 +1,141 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.knox.gateway.webappsec; + +import org.apache.knox.gateway.webappsec.filter.XContentTypeOptionsFilter; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Properties; + +import static org.junit.Assert.fail; + +public class XContentTypeOptionsFilterTest { + + private String options = null; + private Collection<String> headers = null; + + @Test + public void testDefaultOptionsValue() throws Exception { + try { + final String expectedDefaultValue = XContentTypeOptionsFilter.DEFAULT_OPTION_VALUE; + + XContentTypeOptionsFilter filter = new XContentTypeOptionsFilter(); + Properties props = new Properties(); + props.put("xcontent-type.options.enabled", "true"); + filter.init(new TestFilterConfig(props)); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.replay(request); + EasyMock.replay(response); + + TestFilterChain chain = new TestFilterChain(); + filter.doFilter(request, response, chain); + Assert.assertTrue("doFilterCalled should not be false.", chain.doFilterCalled ); + Assert.assertEquals(XContentTypeOptionsFilter.X_CONTENT_TYPE_OPTIONS_HEADER + " value incorrect.", + expectedDefaultValue, options); + Assert.assertEquals(XContentTypeOptionsFilter.X_CONTENT_TYPE_OPTIONS_HEADER + " count incorrect.", + 1, headers.size()); + } catch (ServletException se) { + fail("Should NOT have thrown a ServletException."); + } + } + + @Test + public void testConfiguredOptionsValue() throws Exception { + try { + final String customOption = "myContentTypeOpts"; + + XContentTypeOptionsFilter filter = new XContentTypeOptionsFilter(); + Properties props = new Properties(); + props.put("xcontent-type.options.enabled", "true"); + props.put("xcontent-type.options", customOption); + filter.init(new TestFilterConfig(props)); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.replay(request); + EasyMock.replay(response); + + TestFilterChain chain = new TestFilterChain(); + filter.doFilter(request, response, chain); + Assert.assertTrue("doFilterCalled should not be false.", chain.doFilterCalled ); + Assert.assertEquals(XContentTypeOptionsFilter.X_CONTENT_TYPE_OPTIONS_HEADER + " value incorrect", + customOption, options); + Assert.assertEquals(XContentTypeOptionsFilter.X_CONTENT_TYPE_OPTIONS_HEADER + " count incorrect.", + 1, headers.size()); + } catch (ServletException se) { + fail("Should NOT have thrown a ServletException."); + } + } + + private static class TestFilterConfig implements FilterConfig { + Properties props; + + TestFilterConfig(Properties props) { + this.props = props; + } + + @Override + public String getFilterName() { + return null; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public String getInitParameter(String name) { + return props.getProperty(name, null); + } + + @Override + public Enumeration<String> getInitParameterNames() { + return null; + } + + } + + class TestFilterChain implements FilterChain { + boolean doFilterCalled = false; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) { + doFilterCalled = true; + options = ((HttpServletResponse)response).getHeader(XContentTypeOptionsFilter.X_CONTENT_TYPE_OPTIONS_HEADER); + headers = ((HttpServletResponse)response).getHeaders(XContentTypeOptionsFilter.X_CONTENT_TYPE_OPTIONS_HEADER); + } + + } + + +}