]> source.dussan.org Git - sonarqube.git/commitdiff
[SONAR-18809] replace hash by nonce for CSP headers
authorSteve Marion <unknown>
Fri, 7 Apr 2023 08:43:20 +0000 (10:43 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 11 Apr 2023 20:03:15 +0000 (20:03 +0000)
server/sonar-auth-saml/src/main/resources/samlAuthResult.html
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlAuthStatusPageGeneratorTest.java
server/sonar-auth-saml/src/test/resources/samlAuthResultEmpty.html [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SamlValidationCspHeaders.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SamlValidationRedirectionFilter.java
server/sonar-webserver-auth/src/main/resources/validation-redirection.html
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SamlValidationCspHeadersTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SamlValidationRedirectionFilterTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/saml/ws/ValidationAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/saml/ws/ValidationActionTest.java

index a49503aa659f35ce3e4aa2d6a10b259b27cce1a7..c7cf2554b3a762a98710cccee9b2f386bc3b2828 100644 (file)
       <div id="response" data-response="%SAML_AUTHENTICATION_STATUS%"></div>
     </div>
 
-    <script>
+    <script nonce="%NONCE%">
       window.addEventListener('DOMContentLoaded', (event) => {
 
       function createBox() {
index 440279c227b1a91b5db988120272db1a2377bdef..9fd398321f21bc06ea0160d54a0f5838d00926a4 100644 (file)
  */
 package org.sonar.auth.saml;
 
-import com.google.common.io.Resources;
-import java.io.IOException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
 import javax.servlet.http.HttpServletRequest;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.auth.saml.SamlAuthStatusPageGenerator.getSamlAuthStatusHtml;
 
 public class SamlAuthStatusPageGeneratorTest {
-  private static final String EMPTY_HTML_TEMPLATE_NAME = "samlAuthResultEmpty.html";
+  private static final String EMPTY_DATA_RESPONSE = "eyJ3YXJuaW5ncyI6W10sImF2YWlsYWJsZUF0dHJpYnV0ZXMiOnt9LCJlcnJvcnMiOltdLCJtYXBwZWRBdHRyaWJ1dGVzIjp7fX0=";
 
   @Test
-  public void test_full_html_generation_with_empty_values() {
+  public void getSamlAuthStatusHtml_whenCalled_shouldGeneratePageWithData() {
     SamlAuthenticationStatus samlAuthenticationStatus = mock(SamlAuthenticationStatus.class);
     HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
 
@@ -49,19 +45,7 @@ public class SamlAuthStatusPageGeneratorTest {
     when(httpServletRequest.getContextPath()).thenReturn("context");
 
     String completeHtmlTemplate = getSamlAuthStatusHtml(httpServletRequest, samlAuthenticationStatus);
-    String expectedTemplate = loadTemplateFromResources(EMPTY_HTML_TEMPLATE_NAME);
 
-    assertEquals(expectedTemplate, completeHtmlTemplate);
-
-  }
-
-  private String loadTemplateFromResources(String templateName) {
-    URL url = Resources.getResource(templateName);
-    try {
-      return Resources.toString(url, StandardCharsets.UTF_8);
-    } catch (IOException e) {
-      throw new IllegalStateException(e);
-    }
+    assertThat(completeHtmlTemplate).contains(EMPTY_DATA_RESPONSE);
   }
-
 }
diff --git a/server/sonar-auth-saml/src/test/resources/samlAuthResultEmpty.html b/server/sonar-auth-saml/src/test/resources/samlAuthResultEmpty.html
deleted file mode 100644 (file)
index b592d67..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <link rel="apple-touch-icon" href="context/apple-touch-icon.png" />
-    <link rel="apple-touch-icon" sizes="57x57" href="context/apple-touch-icon-57x57.png" />
-    <link rel="apple-touch-icon" sizes="60x60" href="context/apple-touch-icon-60x60.png" />
-    <link rel="apple-touch-icon" sizes="72x72" href="context/apple-touch-icon-72x72.png" />
-    <link rel="apple-touch-icon" sizes="76x76" href="context/apple-touch-icon-76x76.png" />
-    <link
-      rel="apple-touch-icon"
-      sizes="114x114"
-      href="context/apple-touch-icon-114x114.png"
-    />
-    <link
-      rel="apple-touch-icon"
-      sizes="120x120"
-      href="context/apple-touch-icon-120x120.png"
-    />
-    <link
-      rel="apple-touch-icon"
-      sizes="144x144"
-      href="context/apple-touch-icon-144x144.png"
-    />
-    <link
-      rel="apple-touch-icon"
-      sizes="152x152"
-      href="context/apple-touch-icon-152x152.png"
-    />
-    <link
-      rel="apple-touch-icon"
-      sizes="180x180"
-      href="context/apple-touch-icon-180x180.png"
-    />
-    <link rel="icon" type="image/x-icon" href="context/favicon.ico" />
-    <meta name="application-name" content="SonarQube" />
-    <meta name="msapplication-TileColor" content="#FFFFFF" />
-    <meta name="msapplication-TileImage" content="context/mstile-512x512.png" />
-    <title>SAML Authentication Test</title>
-
-    <style>
-      body {
-        background-color: #f3f3f3;
-      }
-
-      h1 {
-        margin: 0 8px 8px;
-      }
-      h2 {
-        margin: 0 0 8px;
-      }
-
-      ul {
-        list-style: none;
-        margin: 0 8px;
-        padding: 0;
-      }
-
-      li + li {
-        padding-top: 12px;
-        margin-top: 12px;
-        border-top: 1px solid rgba(150, 150, 150, 0.5);
-      }
-
-      table {
-        border-collapse: collapse;
-      }
-
-      tr:nth-child(2n) {
-        background-color: #e6e6e6;
-      }
-
-      td {
-        border: 1px solid #a3a3a3;
-        padding: 4px 24px 4px 8px;
-        vertical-align: top;
-        font-family: "Courier New", Courier, monospace;
-        white-space: pre-line;
-      }
-
-      #content {
-        padding: 16px;
-      }
-
-      .box {
-        padding: 8px;
-        margin: 8px;
-        border: 1px solid #e6e6e6;
-        background-color: white;
-        box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
-      }
-
-      #status {
-        padding: 16px 8px;
-        font-size: large;
-        color: white;
-      }
-
-      .error {
-        background-color: #d02f3a;
-      }
-
-      .success {
-        background-color: #008a25;
-      }
-    </style>
-  </head>
-
-  <body>
-    <div id="content">
-      <h1>SAML Authentication Test</h1>
-      <div class="box">
-        <div id="status"></div>
-      </div>
-      <div id="response" data-response="eyJ3YXJuaW5ncyI6W10sImF2YWlsYWJsZUF0dHJpYnV0ZXMiOnt9LCJlcnJvcnMiOltdLCJtYXBwZWRBdHRyaWJ1dGVzIjp7fX0="></div>
-    </div>
-
-    <script>
-      window.addEventListener('DOMContentLoaded', (event) => {
-
-      function createBox() {
-        const box = document.createElement("div");
-        box.className = "box";
-        return box;
-      }
-
-      function createSectionTitle(title) {
-        const node = document.createElement("h2");
-        node.textContent = title;
-        return node;
-      }
-
-      function createList(arr, className = "") {
-        const list = document.createElement("ul");
-
-        arr.forEach((item) => {
-          const message = document.createElement("li");
-          message.className = className;
-          message.textContent = item;
-          list.appendChild(message);
-        });
-
-        return list;
-      }
-
-      function createTable(obj) {
-        const table = document.createElement("table");
-        const tbody = document.createElement("tbody");
-        table.appendChild(tbody);
-
-        Object.keys(obj).forEach((key) => {
-          const row = document.createElement("tr");
-
-          const keyNode = document.createElement("td");
-          keyNode.textContent = key;
-          row.appendChild(keyNode);
-
-          const valueNode = document.createElement("td");
-          // wrap in array, to handle single values as well
-          valueNode.textContent = [].concat(obj[key]).join("\r\n");
-          row.appendChild(valueNode);
-
-          tbody.appendChild(row);
-        });
-
-        return table;
-      }
-
-      function addSection(container, title, contents) {
-        const box = createBox();
-
-        box.appendChild(createSectionTitle(title));
-        box.appendChild(contents);
-
-        container.appendChild(box);
-      }
-
-      const variables = document.querySelector("#response");
-      const response = variables.dataset.response;
-      const decodedStatus = JSON.parse(atob(response));
-      const status = decodedStatus.status;
-      const attributes = decodedStatus.availableAttributes;
-      const mappings = decodedStatus.mappedAttributes;
-      const errors = decodedStatus.errors;
-      const warnings = decodedStatus.warnings;
-
-      // Switch status class
-      const statusNode = document.querySelector("#status");
-      statusNode.classList.add(status);
-      statusNode.textContent = status;
-
-      // generate content
-      const container = document.querySelector("#content");
-
-      if (warnings && warnings.length > 0) {
-        addSection(container, "Warnings", createList(warnings));
-      }
-
-      if (status === "error" && errors && errors.length > 0) {
-        addSection(container, "Errors", createList(errors));
-      }
-
-      if (status === "success") {
-        if (attributes && Object.keys(attributes).length > 0) {
-          addSection(container, "Available attributes", createTable(attributes));
-        }
-
-        if (mappings && Object.keys(mappings).length > 0) {
-          addSection(container, "Attribute mappings", createTable(mappings));
-        }
-      }
-      });
-    </script>
-  </body>
-</html>
index 2735f4b87ecab64d3b8b9bca3af8c65c249a8dd6..863449a733a10d40006acff6edd78da83d2f2270 100644 (file)
  */
 package org.sonar.server.authentication;
 
-import java.util.Base64;
+import java.math.BigInteger;
+import java.security.SecureRandom;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.codec.digest.DigestUtils;
 
 public class SamlValidationCspHeaders {
 
-  private static final Pattern SCRIPT_PATTERN = Pattern.compile("(?<=<script>)(?s).*(?=</script>)");
-
   private SamlValidationCspHeaders() {
     throw new IllegalStateException("Utility class, cannot be instantiated");
   }
 
-  public static void addCspHeadersToResponse(HttpServletResponse httpResponse, String hash) {
+  public static String addCspHeadersWithNonceToResponse(HttpServletResponse httpResponse) {
+    final String nonce = getNonce();
+
     List<String> cspPolicies = List.of(
       "default-src 'self'",
       "base-uri 'none'",
       "connect-src 'self' http: https:",
       "img-src * data: blob:",
       "object-src 'none'",
-      "script-src 'self' '" + hash + "'",
+      "script-src 'nonce-" + nonce + "'",
       "style-src 'self' 'unsafe-inline'",
       "worker-src 'none'");
     String policies = String.join("; ", cspPolicies).trim();
 
     List<String> cspHeaders = List.of("Content-Security-Policy", "X-Content-Security-Policy", "X-WebKit-CSP");
     cspHeaders.forEach(header -> httpResponse.setHeader(header, policies));
+    return nonce;
   }
 
-  public static String getHashForInlineScript(String html) {
-    Matcher matcher = SCRIPT_PATTERN.matcher(html);
-    if (matcher.find()) {
-      return getBase64Sha256(matcher.group(0));
-    }
-    return "";
+  private static String getNonce() {
+    // this code is the same as in org.sonar.server.authentication.JwtCsrfVerifier.generateState
+    return new BigInteger(130, new SecureRandom()).toString(32);
   }
-
-  private static String getBase64Sha256(String string) {
-    return "sha256-" + Base64.getEncoder().encodeToString(DigestUtils.sha256(string));
-  }
-
 }
index 7298e30e3df550f3ff283cc44840873ddb2334b2..a0d9e55b326b4a63bd7f4138f49d91bc7ee45598 100644 (file)
@@ -37,8 +37,6 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.web.ServletFilter;
 
 import static org.sonar.server.authentication.AuthenticationFilter.CALLBACK_PATH;
-import static org.sonar.server.authentication.SamlValidationCspHeaders.addCspHeadersToResponse;
-import static org.sonar.server.authentication.SamlValidationCspHeaders.getHashForInlineScript;
 
 public class SamlValidationRedirectionFilter extends ServletFilter {
 
@@ -88,12 +86,13 @@ public class SamlValidationRedirectionFilter extends ServletFilter {
       String samlResponse = StringEscapeUtils.escapeHtml(request.getParameter(SAML_RESPONSE_PARAMETER));
       String csrfToken = getCsrfTokenFromRelayState(relayState);
 
+      String nonce = SamlValidationCspHeaders.addCspHeadersWithNonceToResponse(httpResponse);
+
       String template = StringUtils.replaceEachRepeatedly(redirectionPageTemplate,
-        new String[]{"%WEB_CONTEXT%", "%VALIDATION_URL%", "%SAML_RESPONSE%", "%CSRF_TOKEN%"},
-        new String[]{server.getContextPath(), redirectionEndpointUrl.toString(), samlResponse, csrfToken});
+        new String[]{"%NONCE%", "%WEB_CONTEXT%", "%VALIDATION_URL%", "%SAML_RESPONSE%", "%CSRF_TOKEN%"},
+        new String[]{nonce, server.getContextPath(), redirectionEndpointUrl.toString(), samlResponse, csrfToken});
 
       httpResponse.setContentType("text/html");
-      addCspHeadersToResponse(httpResponse, getHashForInlineScript(template));
       httpResponse.getWriter().print(template);
       return;
     }
index f30601364042d34951f91db83ef424b2a68d9fe1..518c6f16505480c6d2449479fd32285f11c294dd 100644 (file)
@@ -14,7 +14,7 @@
         <button>Click Here to See Result</button>
     </form>
 </body>
-<script>
-    document.getElementById("saml_validate").submit()
+<script nonce="%NONCE%">
+    document.getElementById('saml_validate').submit();
 </script>
 </html>
index 9765357cba9b240d2101ebefd1b21915d50df29f..2ca3c6a6ab61b80e70451339403b19a079f5abc2 100644 (file)
@@ -22,73 +22,22 @@ package org.sonar.server.authentication;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
-import static org.sonar.server.authentication.SamlValidationCspHeaders.addCspHeadersToResponse;
-import static org.sonar.server.authentication.SamlValidationCspHeaders.getHashForInlineScript;
+import static org.sonar.server.authentication.SamlValidationCspHeaders.addCspHeadersWithNonceToResponse;
 
 public class SamlValidationCspHeadersTest {
 
   @Test
-  public void CspHeaders_are_correctly_added_to_response() {
+  public void addCspHeadersWithNonceToResponse_whenCalled_shouldAddNonceToCspHeaders() {
     HttpServletResponse httpServletResponse = mock(HttpServletResponse.class);
 
-    addCspHeadersToResponse(httpServletResponse, "hash");
-    verify(httpServletResponse).setHeader("Content-Security-Policy", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' 'hash'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-    verify(httpServletResponse).setHeader("X-Content-Security-Policy", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' 'hash'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-    verify(httpServletResponse).setHeader("X-WebKit-CSP", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' 'hash'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-  }
-
-  @Test
-  public void hash_is_properly_calculated_for_an_inline_script() {
-    String hash = getHashForInlineScript(getBasicHtmlWithScript());
-    assertEquals("sha256-jRoPhEx/vXxIUUkuTwJJ2ww4OPlo7B2ZK/wDVC4IXUs=", hash);
-  }
-
-  @Test
-  public void hash_is_empty_when_no_inline_script_available() {
-    String hash = getHashForInlineScript(getBasicHtmlWithoutScript());
-    assertEquals("", hash);
-  }
-
-  private String getBasicHtmlWithScript() {
-    return """
-      <!DOCTYPE html>
-      <html lang="en">
-        <head>
-          <title>SAML Authentication Test</title>
-        </head>
-        <body>
-          <script>
-            function createBox() {
-              const box = document.createElement("div");
-              box.className = "box";
-              return box;
-            });
-          </script>
-        </body>
-      </html>
-      """;
-  }
+    String nonce = addCspHeadersWithNonceToResponse(httpServletResponse);
 
-  private String getBasicHtmlWithoutScript() {
-    return """
-      <!DOCTYPE html>
-      <html lang="en">
-        <head>
-          <title>SAML Authentication Test</title>
-        </head>  
-        <body>
-          <div id="content">
-            <h1>SAML Authentication Test</h1>
-            <div class="box">
-              <div id="status"></div>
-            </div>
-            <div id="response" data-response="%SAML_AUTHENTICATION_STATUS%"></div>
-          </div>
-        </body>
-      </html>
-      """;
+    verify(httpServletResponse).setHeader(eq("Content-Security-Policy"), contains("script-src 'nonce-" + nonce + "'"));
+    verify(httpServletResponse).setHeader(eq("X-Content-Security-Policy"), contains("script-src 'nonce-" + nonce + "'"));
+    verify(httpServletResponse).setHeader(eq("X-WebKit-CSP"), contains("script-src 'nonce-" + nonce + "'"));
   }
 }
index 38e8ea8cefb0379bf7ee1ab50c2370a08de4300a..bfa90a614ee90c7a52d3da29e5d6025b9bd2f46e 100644 (file)
@@ -24,6 +24,7 @@ import com.tngtech.java.junit.dataprovider.DataProviderRunner;
 import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.List;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
 import javax.servlet.ServletException;
@@ -33,10 +34,13 @@ import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.internal.verification.VerificationModeFactory;
 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.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.matches;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -47,6 +51,8 @@ import static org.mockito.Mockito.when;
 @RunWith(DataProviderRunner.class)
 public class SamlValidationRedirectionFilterTest {
 
+  public static final List<String> CSP_HEADERS = List.of("Content-Security-Policy", "X-Content-Security-Policy", "X-WebKit-CSP");
+
   SamlValidationRedirectionFilter underTest;
 
   @Before
@@ -57,6 +63,7 @@ public class SamlValidationRedirectionFilterTest {
     underTest.init(mock(FilterConfig.class));
   }
 
+
   @Test
   public void do_get_pattern() {
     assertThat(underTest.doGetPattern().matches("/oauth2/callback/saml")).isTrue();
@@ -82,7 +89,7 @@ public class SamlValidationRedirectionFilterTest {
 
     ArgumentCaptor<String> htmlProduced = ArgumentCaptor.forClass(String.class);
     verify(pw).print(htmlProduced.capture());
-    verifyResponseContentTypeAndCSPHeaders(servletResponse, "sha256-TClpsoWi64Z74Xuk4Fa3bdt7mY/7K+A2jHOgNpxDy2I=");
+    CSP_HEADERS.forEach(h -> verify(servletResponse).setHeader(eq(h), anyString()));
     assertThat(htmlProduced.getValue()).contains(validSample);
     assertThat(htmlProduced.getValue()).contains("action=\"contextPath/saml/validation\"");
     assertThat(htmlProduced.getValue()).contains("value=\"CSRF_TOKEN\"");
@@ -107,7 +114,7 @@ public class SamlValidationRedirectionFilterTest {
 
     ArgumentCaptor<String> htmlProduced = ArgumentCaptor.forClass(String.class);
     verify(pw).print(htmlProduced.capture());
-    verifyResponseContentTypeAndCSPHeaders(servletResponse, "sha256-TClpsoWi64Z74Xuk4Fa3bdt7mY/7K+A2jHOgNpxDy2I=");
+    CSP_HEADERS.forEach(h -> verify(servletResponse).setHeader(eq(h), anyString()));
     assertThat(htmlProduced.getValue()).contains(validSample);
     assertThat(htmlProduced.getValue()).doesNotContain("<script>/*Malicious Token*/</script>");
 
@@ -129,9 +136,8 @@ public class SamlValidationRedirectionFilterTest {
     underTest.doFilter(servletRequest, servletResponse, filterChain);
 
     ArgumentCaptor<String> htmlProduced = ArgumentCaptor.forClass(String.class);
-
     verify(pw).print(htmlProduced.capture());
-    verifyResponseContentTypeAndCSPHeaders(servletResponse, "sha256-TClpsoWi64Z74Xuk4Fa3bdt7mY/7K+A2jHOgNpxDy2I=");
+    CSP_HEADERS.forEach(h -> verify(servletResponse).setHeader(eq(h), anyString()));
     assertThat(htmlProduced.getValue()).doesNotContain("<script>/*hack website*/</script>");
     assertThat(htmlProduced.getValue()).contains("action=\"contextPath/saml/validation\"");
   }
@@ -142,8 +148,8 @@ public class SamlValidationRedirectionFilterTest {
     HttpServletRequest servletRequest = mock(HttpServletRequest.class);
     HttpServletResponse servletResponse = mock(HttpServletResponse.class);
     FilterChain filterChain = mock(FilterChain.class);
-
     doReturn(relayStateValue).when(servletRequest).getParameter("RelayState");
+
     underTest.doFilter(servletRequest, servletResponse, filterChain);
 
     verifyNoInteractions(servletResponse);
@@ -158,12 +164,4 @@ public class SamlValidationRedirectionFilterTest {
   public static Object[] invalidRelayStateValues() {
     return new Object[]{"random_query", "validation-query", null};
   }
-
-  private static void verifyResponseContentTypeAndCSPHeaders(HttpServletResponse servletResponse, String hash) {
-    verify(servletResponse).setContentType("text/html");
-    verify(servletResponse).setHeader("Content-Security-Policy", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' '" + hash + "'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-    verify(servletResponse).setHeader("X-Content-Security-Policy", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' '" + hash + "'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-    verify(servletResponse).setHeader("X-WebKit-CSP", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' '" + hash + "'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-  }
-
 }
index 98238ab0b40a48f454587d408d64e391b9d3b2e8..c55a99f37f763194dbbbea317db5311aa69a9ad5 100644 (file)
@@ -36,13 +36,12 @@ import org.sonar.auth.saml.SamlIdentityProvider;
 import org.sonar.server.authentication.AuthenticationError;
 import org.sonar.server.authentication.OAuth2ContextFactory;
 import org.sonar.server.authentication.OAuthCsrfVerifier;
+import org.sonar.server.authentication.SamlValidationCspHeaders;
 import org.sonar.server.authentication.SamlValidationRedirectionFilter;
 import org.sonar.server.authentication.event.AuthenticationException;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.ws.ServletFilterHandler;
 
-import static org.sonar.server.authentication.SamlValidationCspHeaders.addCspHeadersToResponse;
-import static org.sonar.server.authentication.SamlValidationCspHeaders.getHashForInlineScript;
 import static org.sonar.server.saml.ws.SamlValidationWs.SAML_VALIDATION_CONTROLLER;
 
 public class ValidationAction extends ServletFilter implements SamlAction {
@@ -96,7 +95,8 @@ public class ValidationAction extends ServletFilter implements SamlAction {
     httpResponse.setContentType("text/html");
 
     String htmlResponse = samlAuthenticator.getAuthenticationStatusPage(httpRequest, httpResponse);
-    addCspHeadersToResponse(httpResponse, getHashForInlineScript(htmlResponse));
+    String nonce = SamlValidationCspHeaders.addCspHeadersWithNonceToResponse(httpResponse);
+    htmlResponse = htmlResponse.replace("%NONCE%", nonce);
     httpResponse.getWriter().print(htmlResponse);
   }
 
index 405b35932b6096ab7af6cbdd55a623bea02ceaca..659f9dc956a082ac4a3f71b7a712c718c34f720e 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.saml.ws;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.List;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -33,6 +34,7 @@ import org.sonar.auth.saml.SamlAuthenticator;
 import org.sonar.auth.saml.SamlIdentityProvider;
 import org.sonar.server.authentication.OAuth2ContextFactory;
 import org.sonar.server.authentication.OAuthCsrfVerifier;
+import org.sonar.server.authentication.SamlValidationCspHeaders;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
 import org.sonar.server.user.ThreadLocalUserSession;
@@ -40,23 +42,23 @@ import org.sonar.server.user.ThreadLocalUserSession;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
-import static org.sonar.server.authentication.SamlValidationCspHeaders.getHashForInlineScript;
 
 public class ValidationActionTest {
 
   private ValidationAction underTest;
   private SamlAuthenticator samlAuthenticator;
   private ThreadLocalUserSession userSession;
-
   private OAuthCsrfVerifier oAuthCsrfVerifier;
-
   private SamlIdentityProvider samlIdentityProvider;
+  public static final List<String> CSP_HEADERS = List.of("Content-Security-Policy", "X-Content-Security-Policy", "X-WebKit-CSP");
 
   @Before
   public void setup() {
@@ -68,6 +70,7 @@ public class ValidationActionTest {
     underTest = new ValidationAction(userSession, samlAuthenticator, oAuth2ContextFactory, samlIdentityProvider, oAuthCsrfVerifier);
   }
 
+
   @Test
   public void do_get_pattern() {
     assertThat(underTest.doGetPattern().matches("/saml/validation")).isTrue();
@@ -87,14 +90,15 @@ public class ValidationActionTest {
 
     doReturn(true).when(userSession).hasSession();
     doReturn(true).when(userSession).isSystemAdministrator();
-    doReturn(getBasicHtmlWithScript()).when(samlAuthenticator).getAuthenticationStatusPage(any(), any());
+    final String mockedHtmlContent = "mocked html content";
+    doReturn(mockedHtmlContent).when(samlAuthenticator).getAuthenticationStatusPage(any(), any());
 
     underTest.doFilter(servletRequest, servletResponse, filterChain);
 
     verify(samlAuthenticator).getAuthenticationStatusPage(any(), any());
     verify(servletResponse).getWriter();
-    verifyResponseTypeAndCSPHeaders(servletResponse, getHashForInlineScript(getBasicHtmlWithScript()));
-    assertEquals(stringWriter.toString(), getBasicHtmlWithScript());
+    CSP_HEADERS.forEach(h -> verify(servletResponse).setHeader(eq(h), anyString()));
+    assertEquals(mockedHtmlContent, stringWriter.toString());
   }
 
   @Test
@@ -148,58 +152,4 @@ public class ValidationActionTest {
     assertThat(validationInitAction.description()).isNotEmpty();
     assertThat(validationInitAction.handler()).isNotNull();
   }
-
-  private static void verifyResponseTypeAndCSPHeaders(HttpServletResponse servletResponse, String hash) {
-    verify(servletResponse).setContentType("text/html");
-    verify(servletResponse).setHeader("Content-Security-Policy", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' '" + hash + "'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-    verify(servletResponse).setHeader("X-Content-Security-Policy", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' '" + hash + "'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-    verify(servletResponse).setHeader("X-WebKit-CSP", "default-src 'self'; base-uri 'none'; connect-src 'self' http: https:; img-src * data: blob:; object-src 'none'; script-src 'self' '" + hash + "'; style-src 'self' 'unsafe-inline'; worker-src 'none'");
-  }
-
-  private String getBasicHtmlWithScript() {
-    return """
-      <!DOCTYPE html>
-      <html lang="en">
-        <head>
-          <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" />
-          <link rel="icon" type="image/x-icon" href="%WEB_CONTEXT%/favicon.ico" />
-          <meta name="application-name" content="SonarQube" />
-          <meta name="msapplication-TileColor" content="#FFFFFF" />
-          <meta name="msapplication-TileImage" content="%WEB_CONTEXT%/mstile-512x512.png" />
-          <title>SAML Authentication Test</title>
-            
-          <style>
-            .error {
-              background-color: #d02f3a;
-            }
-            
-            .success {
-              background-color: #008a25;
-            }
-          </style>
-        </head>
-            
-        <body>
-          <div id="content">
-            <h1>SAML Authentication Test</h1>
-            <div class="box">
-              <div id="status"></div>
-            </div>
-            <div id="response" data-response="%SAML_AUTHENTICATION_STATUS%"></div>
-          </div>
-            
-          <script>
-            window.addEventListener('DOMContentLoaded', (event) => {
-            
-            function createBox() {
-              const box = document.createElement("div");
-              box.className = "box";
-              return box;
-            }
-            });
-          </script>
-        </body>
-      </html>
-      """;
-  }
 }