]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6881 backport SecurityServletFilter to 4.5 556/head
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 1 Oct 2015 11:53:00 +0000 (13:53 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 1 Oct 2015 11:53:00 +0000 (13:53 +0200)
server/sonar-server/src/main/java/org/sonar/server/platform/SecurityServletFilter.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/SecurityServletFilterTest.java [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/config/environment.rb
server/sonar-web/src/main/webapp/WEB-INF/web.xml

diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/SecurityServletFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/SecurityServletFilter.java
new file mode 100644 (file)
index 0000000..98d0848
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform;
+
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.util.Set;
+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.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This servlet filter sets response headers that enable browser protection against several classes if Web attacks.
+ * The list of headers is mirrored in environment.rb as a workaround to Rack swallowing the headers..
+ */
+public class SecurityServletFilter implements Filter {
+
+  private static final Set<String> ALLOWED_HTTP_METHODS = ImmutableSet.of("DELETE", "GET", "HEAD", "POST", "PUT");
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    // nothing
+  }
+
+  @Override
+  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
+    doHttpFilter((HttpServletRequest) req, (HttpServletResponse) resp, chain);
+  }
+
+  private static void doHttpFilter(HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain chain) throws IOException, ServletException {
+    // SONAR-6881 Disable OPTIONS and TRACE methods
+    if (!ALLOWED_HTTP_METHODS.contains(httpRequest.getMethod())) {
+      httpResponse.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+      return;
+    }
+
+    chain.doFilter(httpRequest, httpResponse);
+
+    // Clickjacking protection
+    // See https://www.owasp.org/index.php/Clickjacking_Protection_for_Java_EE
+    httpResponse.addHeader("X-Frame-Options", "SAMEORIGIN");
+
+    // Cross-site scripting
+    // See https://www.owasp.org/index.php/List_of_useful_HTTP_headers
+    httpResponse.addHeader("X-XSS-Protection", "1; mode=block");
+
+    // MIME-sniffing
+    // See https://www.owasp.org/index.php/List_of_useful_HTTP_headers
+    httpResponse.addHeader("X-Content-Type-Options", "nosniff");
+  }
+
+  @Override
+  public void destroy() {
+    // nothing
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/SecurityServletFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/SecurityServletFilterTest.java
new file mode 100644 (file)
index 0000000..256a806
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform;
+
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.startsWith;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SecurityServletFilterTest {
+
+  SecurityServletFilter underTest = new SecurityServletFilter();
+  HttpServletResponse response = mock(HttpServletResponse.class);
+  FilterChain chain = mock(FilterChain.class);
+
+  @Test
+  public void allow_GET_method() throws IOException, ServletException {
+    assertThatMethodIsAllowed("GET");
+  }
+
+  @Test
+  public void allow_HEAD_method() throws IOException, ServletException {
+    assertThatMethodIsAllowed("HEAD");
+  }
+
+  @Test
+  public void allow_PUT_method() throws IOException, ServletException {
+    assertThatMethodIsAllowed("PUT");
+  }
+
+  @Test
+  public void allow_POST_method() throws IOException, ServletException {
+    assertThatMethodIsAllowed("POST");
+  }
+
+  private void assertThatMethodIsAllowed(String httpMethod) throws IOException, ServletException {
+    HttpServletRequest request = newRequest(httpMethod);
+    underTest.doFilter(request, response, chain);
+    verify(response, never()).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+    verify(chain).doFilter(request, response);
+  }
+
+  @Test
+  public void deny_OPTIONS_method() throws IOException, ServletException {
+    assertThatMethodIsDenied("OPTIONS");
+  }
+
+  @Test
+  public void deny_TRACE_method() throws IOException, ServletException {
+    assertThatMethodIsDenied("TRACE");
+  }
+
+  private void assertThatMethodIsDenied(String httpMethod) throws IOException, ServletException {
+    underTest.doFilter(newRequest(httpMethod), response, chain);
+    verify(response).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+  }
+
+  @Test
+  public void set_secured_headers() throws ServletException, IOException {
+    underTest.init(mock(FilterConfig.class));
+    HttpServletRequest request = newRequest("GET");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response, times(3)).addHeader(startsWith("X-"), anyString());
+
+    underTest.destroy();
+  }
+
+  private HttpServletRequest newRequest(String httpMethod) {
+    HttpServletRequest req = mock(HttpServletRequest.class);
+    when(req.getMethod()).thenReturn(httpMethod);
+    return req;
+  }
+}
index cbe9edcf3158ebcbabf3e4bf3674a3391b8e62fe..b93dc9152dc908407d48b6d3444e73e99b598e7d 100644 (file)
@@ -52,6 +52,35 @@ class EagerPluginLoader < Rails::Plugin::Loader
   end
 end
 
+#
+# Put response headers on all HTTP calls. This is done by the Java SecurityServlerFilter,
+# but for some reason Rack swallows the headers set on Java side.
+# See middleware configuration below.
+#
+class SecurityHeaders
+  def initialize(app)
+    @app = app
+  end
+
+  def call(env)
+    status, headers, body = @app.call(env)
+
+    # Clickjacking protection
+    # See https://www.owasp.org/index.php/Clickjacking_Protection_for_Java_EE
+    headers['X-Frame-Options']='SAMEORIGIN'
+
+    # Cross-site scripting
+    # See https://www.owasp.org/index.php/List_of_useful_HTTP_headers
+    headers['X-XSS-Protection']='1; mode=block'
+
+    # MIME-sniffing
+    # See https://www.owasp.org/index.php/List_of_useful_HTTP_headers
+    headers['X-Content-Type-Options']='nosniff';
+
+    [status, headers, body]
+  end
+end
+
 Rails::Initializer.run do |config|
   # Settings in config/environments/* take precedence over those specified here.
   # Application configuration should go into files in config/initializers
@@ -108,6 +137,9 @@ Rails::Initializer.run do |config|
   # Activate observers that should always be running
   # Please note that observers generated using script/generate observer need to have an _observer suffix
   # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+
+  # Add security related headers
+  config.middleware.use SecurityHeaders
 end
 
 
index b4896f15c5666835603c75e544f995c70303b3f2..bb2cc8d743b2623fa93c7db039bc5bcff04090b6 100644 (file)
     <filter-name>RackFilter</filter-name>
     <filter-class>org.jruby.rack.RackFilter</filter-class>
   </filter>
+  <filter>
+    <filter-name>SecurityFilter</filter-name>
+    <filter-class>org.sonar.server.platform.SecurityServletFilter</filter-class>
+  </filter>
   <filter>
     <filter-name>ProfilingFilter</filter-name>
     <filter-class>org.sonar.server.platform.ProfilingFilter</filter-class>
     <filter-name>ServletFilters</filter-name>
     <url-pattern>/*</url-pattern>
   </filter-mapping>
+  <filter-mapping>
+    <filter-name>SecurityFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+  </filter-mapping>
   <filter-mapping>
     <filter-name>RackFilter</filter-name>
     <url-pattern>/*</url-pattern>