]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17296 fix SAML validation page not being accessible with a user session.
authorSteve Marion <steve.marion@sonarsource.com>
Tue, 20 Sep 2022 15:05:22 +0000 (17:05 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 22 Sep 2022 20:03:32 +0000 (20:03 +0000)
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SamlValidationRedirectionFilter.java
server/sonar-webserver-auth/src/main/resources/validation-redirection.html [new file with mode: 0644]
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SamlValidationRedirectionFilterTest.java

index d28daf1b72ae987f9871c16ab25bc1ea6cd9787e..9a76357e5f6d2e6ac4c0c768adb891f23e0b4c03 100644 (file)
 
 package org.sonar.server.authentication;
 
+import com.google.common.io.Resources;
 import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 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;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.internal.apachecommons.lang.StringEscapeUtils;
 import org.sonar.api.platform.Server;
 import org.sonar.api.web.ServletFilter;
 
@@ -36,6 +42,7 @@ public class SamlValidationRedirectionFilter extends ServletFilter {
 
   public static final String VALIDATION_RELAY_STATE = "validation-query";
   public static final String SAML_VALIDATION_URL = "/saml/validation_callback";
+  private String redirectionPageTemplate;
   private final Server server;
 
   public SamlValidationRedirectionFilter(Server server) {
@@ -47,13 +54,35 @@ public class SamlValidationRedirectionFilter extends ServletFilter {
     return UrlPattern.create(CALLBACK_PATH + "saml");
   }
 
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    super.init(filterConfig);
+    this.redirectionPageTemplate = extractTemplate("validation-redirection.html");
+  }
+
+  String extractTemplate(String templateLocation) {
+    try {
+      URL url = Resources.getResource(templateLocation);
+      return Resources.toString(url, StandardCharsets.UTF_8);
+    } catch (IOException | IllegalArgumentException e) {
+      throw new IllegalStateException("Cannot read the template " + templateLocation, e);
+    }
+  }
+
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
     HttpServletRequest httpRequest = (HttpServletRequest) request;
     if (isSamlValidation(httpRequest)) {
       HttpServletResponse httpResponse = (HttpServletResponse) response;
-      httpResponse.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
-      httpResponse.setHeader("Location", server.getContextPath() + SAML_VALIDATION_URL);
+
+      String samlResponse = StringEscapeUtils.escapeHtml(request.getParameter("SAMLResponse"));
+
+      String template = StringUtils.replaceEachRepeatedly(redirectionPageTemplate,
+        new String[]{"%VALIDATION_URL%", "%SAML_RESPONSE%"},
+        new String[]{server.getContextPath() + SAML_VALIDATION_URL, samlResponse});
+
+      httpResponse.setContentType("text/html");
+      httpResponse.getWriter().print(template);
       return;
     }
     chain.doFilter(request, response);
diff --git a/server/sonar-webserver-auth/src/main/resources/validation-redirection.html b/server/sonar-webserver-auth/src/main/resources/validation-redirection.html
new file mode 100644 (file)
index 0000000..df9a48a
--- /dev/null
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta name="application-name" content="SonarQube"/>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8"/>
+    <title>SAML Authentication Test</title>
+</head>
+
+<body>
+    <h1>SAML Authentication Validation</h1>
+    <form id="saml_validate" action="%VALIDATION_URL%" method="POST">
+        <input name="SAMLResponse" value="%SAML_RESPONSE%" type="hidden"/>
+        <button>Click Here to See Result</button>
+    </form>
+</body>
+<script>
+    document.getElementById("saml_validate").submit()
+</script>
+</html>
index ba54dddc33a6b912b1b65d18765b37d6f1bd94c1..5dc41eea3cb24be3163a3452e377c63a670887b4 100644 (file)
 package org.sonar.server.authentication;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 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.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.sonar.api.platform.Server;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.matches;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
 
 public class SamlValidationRedirectionFilterTest {
 
   SamlValidationRedirectionFilter underTest;
 
   @Before
-  public void setup() {
+  public void setup() throws ServletException {
     Server server = mock(Server.class);
     doReturn("").when(server).getContextPath();
     underTest = new SamlValidationRedirectionFilter(server);
+    underTest.init(mock(FilterConfig.class));
   }
 
   @Test
@@ -60,11 +67,39 @@ public class SamlValidationRedirectionFilterTest {
     HttpServletResponse servletResponse = mock(HttpServletResponse.class);
     FilterChain filterChain = mock(FilterChain.class);
 
-    doReturn("validation-query").when(servletRequest).getParameter("RelayState");
+    String validSample = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
+    when(servletRequest.getParameter(matches("SAMLResponse"))).thenReturn(validSample);
+    when(servletRequest.getParameter(matches("RelayState"))).thenReturn("validation-query");
+    PrintWriter pw = mock(PrintWriter.class);
+    when(servletResponse.getWriter()).thenReturn(pw);
+
     underTest.doFilter(servletRequest, servletResponse, filterChain);
 
-    verify(servletResponse).setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
-    verify(servletResponse).setHeader("Location", "/saml/validation_callback");
+    verify(servletResponse).setContentType("text/html");
+    ArgumentCaptor<String> htmlProduced = ArgumentCaptor.forClass(String.class);
+    verify(pw).print(htmlProduced.capture());
+    assertThat(htmlProduced.getValue()).contains(validSample);
+  }
+
+  @Test
+  public void do_filter_validation_wrong_SAML_response() throws ServletException, IOException {
+    HttpServletRequest servletRequest = mock(HttpServletRequest.class);
+    HttpServletResponse servletResponse = mock(HttpServletResponse.class);
+    FilterChain filterChain = mock(FilterChain.class);
+
+    String maliciousSaml = "test\"</input><script>/*hack website*/</script><input value=\"";
+
+    when(servletRequest.getParameter(matches("SAMLResponse"))).thenReturn(maliciousSaml);
+    when(servletRequest.getParameter(matches("RelayState"))).thenReturn("validation-query");
+    PrintWriter pw = mock(PrintWriter.class);
+    when(servletResponse.getWriter()).thenReturn(pw);
+
+    underTest.doFilter(servletRequest, servletResponse, filterChain);
+
+    verify(servletResponse).setContentType("text/html");
+    ArgumentCaptor<String> htmlProduced = ArgumentCaptor.forClass(String.class);
+    verify(pw).print(htmlProduced.capture());
+    assertThat(htmlProduced.getValue()).doesNotContain("<script>/*hack website*/</script>");
   }
 
   @Test
@@ -78,4 +113,9 @@ public class SamlValidationRedirectionFilterTest {
 
     verifyNoInteractions(servletResponse);
   }
+
+  @Test
+  public void extract_nonexistant_template() {
+    assertThrows(IllegalStateException.class, () -> underTest.extractTemplate("not-there"));
+  }
 }