package org.sonar.server.platform.web;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
cspPolicies.add("img-src * data: blob:");
cspPolicies.add("object-src 'none'");
// the hash below corresponds to the window.__assetsPath script in index.html
- cspPolicies.add("script-src 'self' 'sha256-D1jaqcDDM2TM2STrzE42NNqyKR9PlptcHDe6tyaBcuM='");
+ cspPolicies.add("script-src 'self' " + getAssetsPathScriptCSPHash(filterConfig.getServletContext().getContextPath()));
cspPolicies.add("style-src 'self' 'unsafe-inline'");
cspPolicies.add("worker-src 'none'");
this.policies = String.join("; ", cspPolicies).trim();
// Not used
}
+ private static String getAssetsPathScriptCSPHash(String contextPath) {
+ final String WEB_CONTEXT_PLACEHOLDER = "WEB_CONTEXT";
+ final String ASSETS_PATH_SCRIPT = "\n" +
+ " window.__assetsPath = function (filename) {\n" +
+ " return 'WEB_CONTEXT/' + filename;\n" +
+ " };\n" +
+ " ";
+
+ String assetsPathScriptWithContextPath = ASSETS_PATH_SCRIPT.replace(WEB_CONTEXT_PLACEHOLDER, contextPath);
+ return generateCSPHash(assetsPathScriptWithContextPath);
+ }
+
+ private static String generateCSPHash(String str) {
+ try {
+ byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
+ byte[] digestBytes = MessageDigest.getInstance("SHA-256").digest(bytes);
+ String rawHash = Base64.getMimeEncoder().encodeToString(digestBytes);
+ return String.format("'%s-%s'", "sha256", rawHash);
+ } catch (NoSuchAlgorithmException e) {
+ return "";
+ }
+ }
}
import org.junit.Before;
import org.junit.Test;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_MOCKS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
"font-src 'self' data:; " +
"img-src * data: blob:; " +
"object-src 'none'; " +
- "script-src 'self' 'sha256-D1jaqcDDM2TM2STrzE42NNqyKR9PlptcHDe6tyaBcuM='; " +
+ "script-src 'self' 'sha256-hK8SVWFNHY0UhP61DBzX/3fvT74EI8u6/jRQvUKeZoU='; " +
"style-src 'self' 'unsafe-inline'; " +
"worker-src 'none'";
private final ServletContext servletContext = mock(ServletContext.class, RETURNS_MOCKS);
@Before
public void setUp() throws ServletException {
- when(servletContext.getContextPath()).thenReturn(TEST_CONTEXT);
+ when(config.getServletContext()).thenReturn(servletContext);
}
@Test
public void set_content_security_headers() throws Exception {
+ when(servletContext.getContextPath()).thenReturn(TEST_CONTEXT);
doInit();
HttpServletRequest request = newRequest("/");
underTest.doFilter(request, response, chain);
verify(chain).doFilter(request, response);
}
+ @Test
+ public void csp_hash_should_be_correct_without_a_context_path() throws Exception {
+ when(servletContext.getContextPath()).thenReturn("");
+ doInit();
+ HttpServletRequest request = newRequest("/");
+ underTest.doFilter(request, response, chain);
+ verify(response).setHeader(eq("Content-Security-Policy"), contains("script-src 'self' 'sha256-D1jaqcDDM2TM2STrzE42NNqyKR9PlptcHDe6tyaBcuM='; "));
+ verify(chain).doFilter(request, response);
+ }
+
private void doInit() throws ServletException {
underTest.init(config);
}