This is an automated email from the ASF dual-hosted git repository.

lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 9b653160c KNOX-3124 - Add Security Header filter to WebAppSec Provider 
(#1021)
9b653160c is described below

commit 9b653160c94e6b41b0d521c62466af1ef8b38aeb
Author: lmccay <[email protected]>
AuthorDate: Tue Apr 15 21:16:45 2025 -0400

    KNOX-3124 - Add Security Header filter to WebAppSec Provider (#1021)
    
    * KNOX-3124 - Add Security Header filter to WebAppSec Provider
---
 .../webappsec/deploy/WebAppSecContributor.java     |  54 +++++----
 .../webappsec/filter/SecurityHeaderFilter.java     | 125 +++++++++++++++++++++
 .../webappsec/SecurityHeaderFilterTest.java        | 119 ++++++++++++++++++++
 3 files changed, 279 insertions(+), 19 deletions(-)

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 40f94b08b..0c6ad77e3 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
@@ -55,6 +55,11 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
   private static final String RATE_LIMITING_PREFIX = "rate.limiting";
   private static final String RATE_LIMITING_SUFFIX = "_RATE.LIMITING";
   private static final String RATE_LIMITING_ENABLED = RATE_LIMITING_PREFIX + 
".enabled";
+  private static final String SECURITY_HEADER_PREFIX = "security.header.";
+  private static final String SECURITY_HEADER_ENABLED = SECURITY_HEADER_PREFIX 
+ ".enabled";
+  private static final String SECURITY_HEADER_SUFFIX = "_SECURITY.HEADER";
+  private static final String SECURITY_HEADER_FILTER_CLASSNAME = 
"org.apache.knox.gateway.webappsec.filter.SecurityHeaderFilter";
+  public static final String ENABLED = "enabled";
 
   @Override
   public String getRole() {
@@ -80,7 +85,7 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
 
     Provider webappsec = context.getTopology().getProvider(ROLE, NAME);
     if (webappsec != null && webappsec.isEnabled()) {
-      Map<String,String> map = provider.getParams();
+      Map<String, String> map = provider.getParams();
       if (params == null) {
         params = new ArrayList<>();
       }
@@ -103,9 +108,9 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
       if (Boolean.parseBoolean(corsEnabled)) {
         provisionConfig(resource, providerParams, params, "cors.");
         resource.addFilter().name(getName() + CORS_SUFFIX)
-                            .role(getRole())
-                            .impl(CORS_FILTER_CLASSNAME)
-                            .params(params);
+                .role(getRole())
+                .impl(CORS_FILTER_CLASSNAME)
+                .params(params);
       }
 
       // CRSF
@@ -114,9 +119,9 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
       if (Boolean.parseBoolean(csrfEnabled)) {
         provisionConfig(resource, providerParams, params, "csrf.");
         resource.addFilter().name(getName() + CSRF_SUFFIX)
-                            .role(getRole())
-                            .impl(CSRF_FILTER_CLASSNAME)
-                            .params(params);
+                .role(getRole())
+                .impl(CSRF_FILTER_CLASSNAME)
+                .params(params);
       }
 
       // X-Frame-Options - clickjacking protection
@@ -125,9 +130,9 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
       if (Boolean.parseBoolean(xframeOptionsEnabled)) {
         provisionConfig(resource, providerParams, params, "xframe.");
         resource.addFilter().name(getName() + XFRAME_OPTIONS_SUFFIX)
-                            .role(getRole())
-                            .impl(XFRAME_OPTIONS_FILTER_CLASSNAME)
-                            .params(params);
+                .role(getRole())
+                .impl(XFRAME_OPTIONS_FILTER_CLASSNAME)
+                .params(params);
       }
 
       // X-Content-Type-Options - MIME type sniffing protection
@@ -136,9 +141,9 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
       if (Boolean.parseBoolean(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);
+                .role(getRole())
+                .impl(XCONTENT_TYPE_OPTIONS_FILTER_CLASSNAME)
+                .params(params);
       }
 
       // X-XSS-Protection - browser xss protection
@@ -147,9 +152,9 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
       if (Boolean.parseBoolean(xssProtectionEnabled)) {
         provisionConfig(resource, providerParams, params, "xss.");
         resource.addFilter().name(getName() + XSS_PROTECTION_SUFFIX)
-                            .role(getRole())
-                            .impl(XSS_PROTECTION_FILTER_CLASSNAME)
-                            .params(params);
+                .role(getRole())
+                .impl(XSS_PROTECTION_FILTER_CLASSNAME)
+                .params(params);
       }
 
       // HTTP Strict-Transport-Security
@@ -158,9 +163,20 @@ public class WebAppSecContributor extends 
ProviderDeploymentContributorBase {
       if (Boolean.parseBoolean(strictTranportEnabled)) {
         provisionConfig(resource, providerParams, params, "strict.");
         resource.addFilter().name(getName() + STRICT_TRANSPORT_SUFFIX)
-                            .role(getRole())
-                            .impl(STRICT_TRANSPORT_FILTER_CLASSNAME)
-                            .params(params);
+                .role(getRole())
+                .impl(STRICT_TRANSPORT_FILTER_CLASSNAME)
+                .params(params);
+      }
+
+      // HTTP Security Headers
+      params = new ArrayList<>();
+      String securityHeaderEnabled = map.get(SECURITY_HEADER_ENABLED);
+      if (Boolean.parseBoolean(securityHeaderEnabled)) {
+        provisionConfig(resource, providerParams, params, 
SECURITY_HEADER_PREFIX, true, false);
+        resource.addFilter().name(getName() + SECURITY_HEADER_SUFFIX)
+                .role(getRole())
+                .impl(SECURITY_HEADER_FILTER_CLASSNAME)
+                .params(params);
       }
     }
   }
diff --git 
a/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/SecurityHeaderFilter.java
 
b/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/SecurityHeaderFilter.java
new file mode 100644
index 000000000..262b64145
--- /dev/null
+++ 
b/gateway-provider-security-webappsec/src/main/java/org/apache/knox/gateway/webappsec/filter/SecurityHeaderFilter.java
@@ -0,0 +1,125 @@
+/*
+ * 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.knox.gateway.webappsec.filter;
+
+import org.apache.knox.gateway.webappsec.deploy.WebAppSecContributor;
+
+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.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class SecurityHeaderFilter implements Filter {
+
+  private Map<String, String> securityHeaders = new HashMap<>();
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    // Dynamically add headers based on init parameters
+    Enumeration<String> initParamNames = filterConfig.getInitParameterNames();
+    while (initParamNames.hasMoreElements()) {
+      String headerName = initParamNames.nextElement();
+      if (!WebAppSecContributor.ENABLED.equals(headerName)) {
+        String headerValue = filterConfig.getInitParameter(headerName);
+        securityHeaders.put(headerName, headerValue);
+      }
+    }
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain)
+          throws IOException, ServletException {
+
+    HttpServletResponse httpResponse = new 
SecurityHeaderResponseWrapper((HttpServletResponse) response);
+
+    // Dynamically add headers based on init parameters
+    for (Map.Entry<String, String> entry : securityHeaders.entrySet()) {
+      String headerName = entry.getKey();
+      String headerValue = entry.getValue();
+      httpResponse.setHeader(headerName, headerValue);
+    }
+
+    // Continue the filter chain
+    chain.doFilter(request, httpResponse);
+  }
+
+  @Override
+  public void destroy() {
+    // Cleanup logic if needed
+  }
+
+  class SecurityHeaderResponseWrapper extends HttpServletResponseWrapper {
+
+    SecurityHeaderResponseWrapper(HttpServletResponse res) {
+      super(res);
+    }
+
+    @Override
+    public void addHeader(String name, String value) {
+      if (!WebAppSecContributor.ENABLED.equals(name)) {
+        super.addHeader(name, value);
+      }
+    }
+
+    @Override
+    public void setHeader(String name, String value) {
+      if (!WebAppSecContributor.ENABLED.equals(name)) {
+        super.setHeader(name, value);
+      }
+    }
+
+    @Override
+    public String getHeader(String name) {
+      String value;
+      value = securityHeaders.get(name);
+      if (value == null) {
+        value = super.getHeader(name);
+      }
+      return value;
+    }
+
+    @Override
+    public Collection<String> getHeaderNames() {
+      Set<String> names = new HashSet<>(securityHeaders.keySet());
+      if (super.getHeaderNames() != null) {
+        names.addAll(super.getHeaderNames());
+      }
+      return names;
+    }
+
+    @Override
+    public Collection<String> getHeaders(String name) {
+      Set<String> values = new HashSet<>(securityHeaders.values());
+      if (super.getHeaders(name) != null) {
+        values.addAll(super.getHeaders(name));
+      }
+      return values;
+    }
+  }
+}
diff --git 
a/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/SecurityHeaderFilterTest.java
 
b/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/SecurityHeaderFilterTest.java
new file mode 100644
index 000000000..fe9511323
--- /dev/null
+++ 
b/gateway-provider-security-webappsec/src/test/java/org/apache/knox/gateway/webappsec/SecurityHeaderFilterTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.SecurityHeaderFilter;
+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.io.IOException;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Properties;
+
+import static org.junit.Assert.fail;
+
+public class SecurityHeaderFilterTest {
+
+  private static final String CONTENT_SECURITY_POLICY = 
"Content-Security-Policy";
+  private String options;
+  private Collection<String> headers;
+  private Collection<String> headerNames;
+
+  @Test
+  public void testConfiguredSecurityHeaderOptionsValue() throws Exception {
+    try {
+      final String customOption = "default-src 'self'";
+
+      SecurityHeaderFilter filter = new SecurityHeaderFilter();
+      Properties props = new Properties();
+      props.put("enabled", "true");
+      props.put(CONTENT_SECURITY_POLICY, 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("SecurityHeaderFilter value incorrect", 
customOption, options);
+      Assert.assertEquals("SecurityHeaderFilter header count incorrect.", 1, 
headers.size());
+      Assert.assertEquals("SecurityHeaderFilter header value incorrect.", 
customOption, headers.toArray()[0]);
+      Assert.assertEquals("SecurityHeaderFilter header name count incorrect.", 
1, headerNames.size());
+      Assert.assertEquals("SecurityHeaderFilter header value count 
incorrect.", CONTENT_SECURITY_POLICY,
+              headerNames.toArray()[0]);
+    } catch (ServletException se) {
+      fail("Should NOT have thrown a ServletException.");
+    }
+  }
+
+  private 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 (Enumeration<String>) props.propertyNames();
+    }
+
+  }
+
+  class TestFilterChain implements FilterChain {
+    boolean doFilterCalled;
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response) 
throws IOException, ServletException {
+      doFilterCalled = true;
+      options = 
((HttpServletResponse)response).getHeader(CONTENT_SECURITY_POLICY);
+      headers = 
((HttpServletResponse)response).getHeaders(CONTENT_SECURITY_POLICY);
+      headerNames = ((HttpServletResponse)response).getHeaderNames();
+    }
+
+  }
+
+
+}

Reply via email to