From d7d1ab5c0faeafc63a7278bb7e775a0de7d9b1b3 Mon Sep 17 00:00:00 2001 From: Sylvain Jermini Date: Thu, 5 Sep 2019 13:12:43 +0200 Subject: [PATCH 1/4] #740 re enable csp --- .../java/alfio/config/MvcConfiguration.java | 61 +------------------ .../alfio/controller/IndexController.java | 32 ++++++++++ 2 files changed, 33 insertions(+), 60 deletions(-) diff --git a/src/main/java/alfio/config/MvcConfiguration.java b/src/main/java/alfio/config/MvcConfiguration.java index 566f0f5f9f..5869b9bf79 100644 --- a/src/main/java/alfio/config/MvcConfiguration.java +++ b/src/main/java/alfio/config/MvcConfiguration.java @@ -16,14 +16,10 @@ */ package alfio.config; -import alfio.manager.system.ConfigurationManager; -import alfio.model.system.ConfigurationKeys; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -36,19 +32,13 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession; import org.springframework.web.multipart.commons.CommonsMultipartResolver; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.*; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.view.AbstractUrlBasedView; import org.springframework.web.servlet.view.UrlBasedViewResolver; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; @Configuration @ComponentScan(basePackages = {"alfio.controller", "alfio.config"}) @@ -56,17 +46,10 @@ @EnableJdbcHttpSession(maxInactiveIntervalInSeconds = 4 * 60 * 60) //4h public class MvcConfiguration implements WebMvcConfigurer { - private final ConfigurationManager configurationManager; private final Environment environment; - private static final Cache configurationCache = Caffeine.newBuilder() - .expireAfterWrite(15, TimeUnit.MINUTES) - .build(); @Autowired - public MvcConfiguration( - ConfigurationManager configurationManager, - Environment environment) { - this.configurationManager = configurationManager; + public MvcConfiguration(Environment environment) { this.environment = environment; } @@ -82,48 +65,6 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { .addResourceLocations("/webjars/"); } - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(getCSPInterceptor()); - } - - private HandlerInterceptor getCSPInterceptor() { - return new HandlerInterceptorAdapter() { - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, - ModelAndView modelAndView) { - - // - String reportUri = ""; - boolean enabledReport = Boolean.parseBoolean(configurationCache.get(ConfigurationKeys.SECURITY_CSP_REPORT_ENABLED, - k -> configurationManager.getForSystem(k).getValueOrDefault("false") - )); - if (enabledReport) { - reportUri = " report-uri " + configurationCache.get(ConfigurationKeys.SECURITY_CSP_REPORT_URI, - k -> configurationManager.getForSystem(k).getValueOrDefault("/report-csp-violation") - ); - } - // - - - // http://www.html5rocks.com/en/tutorials/security/content-security-policy/ - // lockdown policy - - response.addHeader("Content-Security-Policy", "default-src 'none'; "//block all by default - + " script-src 'self' https://js.stripe.com https://checkout.stripe.com/ https://m.stripe.network https://api.stripe.com/ https://ssl.google-analytics.com/ https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/api2/ https://maps.googleapis.com/;"// - + " style-src 'self' 'unsafe-inline';" // unsafe-inline for style is acceptable... - + " img-src 'self' https: data:;"// - + " child-src 'self';" - + " worker-src 'self';"//webworker - + " frame-src 'self' https://js.stripe.com https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://www.google.com;" - + " font-src 'self';"// - + " media-src blob: 'self';"//for loading camera api - + " connect-src 'self' https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://maps.googleapis.com/ https://geocoder.cit.api.here.com;" //<- currently stripe.js use jsonp but if they switch to xmlhttprequest+cors we will be ready - + reportUri); - } - }; - } - @Override public void configureMessageConverters(List> converters) { converters.add(jacksonMessageConverter()); diff --git a/src/main/java/alfio/controller/IndexController.java b/src/main/java/alfio/controller/IndexController.java index bb1e84339e..1082b5d7bf 100644 --- a/src/main/java/alfio/controller/IndexController.java +++ b/src/main/java/alfio/controller/IndexController.java @@ -55,6 +55,7 @@ import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.EnumSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; @@ -131,6 +132,7 @@ public void replyToIndex(@PathVariable(value = "eventShortName", required = fals response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); + addCspHeader(response); if (eventShortName != null && RequestUtils.isSocialMediaShareUA(userAgent) && eventRepository.existsByShortName(eventShortName)) { try (var is = new ClassPathResource("alfio/web-templates/event-open-graph-page.html").getInputStream(); var os = response.getOutputStream()) { @@ -251,6 +253,7 @@ public void getLoginPage(@RequestParam(value="failed", required = false) String try (var os = response.getOutputStream()) { response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); + addCspHeader(response); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/login.ms"), model.asMap(), os); } } @@ -281,7 +284,36 @@ public void adminHome(Model model, @Value("${alfio.version}") String version, Ht try (var os = response.getOutputStream()) { response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); + addCspHeader(response); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/admin-index.ms"), model.asMap(), os); } } + + + + public void addCspHeader(HttpServletResponse response) { + String reportUri = ""; + + var conf = configurationManager.getFor(List.of(ConfigurationKeys.SECURITY_CSP_REPORT_ENABLED, ConfigurationKeys.SECURITY_CSP_REPORT_URI), ConfigurationLevel.system()); + + boolean enabledReport = conf.get(ConfigurationKeys.SECURITY_CSP_REPORT_ENABLED).getValueAsBooleanOrDefault(false); + if (enabledReport) { + reportUri = " report-uri " + conf.get(ConfigurationKeys.SECURITY_CSP_REPORT_URI).getValueOrDefault("/report-csp-violation"); + } + // + // http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + // lockdown policy + + response.addHeader("Content-Security-Policy", "default-src 'none'; "//block all by default + + " script-src 'self' https://js.stripe.com https://checkout.stripe.com/ https://m.stripe.network https://api.stripe.com/ https://ssl.google-analytics.com/ https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/api2/ https://maps.googleapis.com/;"// + + " style-src 'self' 'unsafe-inline';" // unsafe-inline for style is acceptable... + + " img-src 'self' https: data:;"// + + " child-src 'self';" + + " worker-src 'self';"//webworker + + " frame-src 'self' https://js.stripe.com https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://www.google.com;" + + " font-src 'self';"// + + " media-src blob: 'self';"//for loading camera api + + " connect-src 'self' https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://maps.googleapis.com/ https://geocoder.cit.api.here.com;" //<- currently stripe.js use jsonp but if they switch to xmlhttprequest+cors we will be ready + + reportUri); + } } From dbd6c27b26ac79eea5cec75dedd2776008a809dc Mon Sep 17 00:00:00 2001 From: Sylvain Jermini Date: Thu, 5 Sep 2019 15:26:24 +0200 Subject: [PATCH 2/4] #740 update jfiveparse, initial work for propagating the nonce --- build.gradle | 4 +- .../alfio/controller/IndexController.java | 61 +++++-- .../alfio/web-templates/admin-index.ms | 170 +++++++++--------- .../resources/alfio/web-templates/login.ms | 4 +- 4 files changed, 134 insertions(+), 105 deletions(-) diff --git a/build.gradle b/build.gradle index 69fedd3a8c..87bba14f21 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ buildscript { //this is for processing the index.html at compile time classpath "com.github.alfio-event:alf.io-public-frontend:$alfioPublicFrontendVersion" - classpath "ch.digitalfondue.jfiveparse:jfiveparse:0.5.3" + classpath "ch.digitalfondue.jfiveparse:jfiveparse:0.5.4" // } @@ -139,7 +139,7 @@ dependencies { /**/ compile "com.openhtmltopdf:openhtmltopdf-core:1.0.0" compile "com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.0" - compile "ch.digitalfondue.jfiveparse:jfiveparse:0.5.3" + compile "ch.digitalfondue.jfiveparse:jfiveparse:0.5.4" /**/ compile "com.google.zxing:core:3.4.0" compile "com.google.zxing:javase:3.4.0" diff --git a/src/main/java/alfio/controller/IndexController.java b/src/main/java/alfio/controller/IndexController.java index 1082b5d7bf..7233cdcd85 100644 --- a/src/main/java/alfio/controller/IndexController.java +++ b/src/main/java/alfio/controller/IndexController.java @@ -35,6 +35,7 @@ import alfio.util.TemplateManager; import ch.digitalfondue.jfiveparse.*; import lombok.AllArgsConstructor; +import org.apache.commons.codec.binary.Hex; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; @@ -54,10 +55,9 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.security.Principal; -import java.util.EnumSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.security.SecureRandom; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; import static alfio.model.system.ConfigurationKeys.ENABLE_CAPTCHA_FOR_LOGIN; @@ -71,6 +71,23 @@ public class IndexController { private static final String TEXT_HTML_CHARSET_UTF_8 = "text/html;charset=UTF-8"; private static final String UTF_8 = "UTF-8"; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + private static final Document INDEX_PAGE; + private static final Document OPEN_GRAPH_PAGE; + + static { + try (var idxIs = new ClassPathResource("alfio-public-frontend-index.html").getInputStream(); + var idxOpenGraph = new ClassPathResource("alfio-public-frontend-index.html").getInputStream()) { + var parser = new Parser(); + INDEX_PAGE = parser.parse(new InputStreamReader(idxIs, StandardCharsets.UTF_8)); + OPEN_GRAPH_PAGE = parser.parse(new InputStreamReader(idxOpenGraph, StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + private final ConfigurationManager configurationManager; private final EventRepository eventRepository; private final Environment environment; @@ -132,23 +149,24 @@ public void replyToIndex(@PathVariable(value = "eventShortName", required = fals response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); - addCspHeader(response); + var nonce = addCspHeader(response); if (eventShortName != null && RequestUtils.isSocialMediaShareUA(userAgent) && eventRepository.existsByShortName(eventShortName)) { - try (var is = new ClassPathResource("alfio/web-templates/event-open-graph-page.html").getInputStream(); var os = response.getOutputStream()) { - var res = getOpenGraphPage(is, eventShortName, request, lang); + try (var os = response.getOutputStream()) { + var res = getOpenGraphPage((Document) OPEN_GRAPH_PAGE.cloneNode(true), eventShortName, request, lang); os.write(res); } } else { - try (var is = new ClassPathResource("alfio-public-frontend-index.html").getInputStream(); var os = response.getOutputStream()) { - is.transferTo(os); + try (var os = response.getOutputStream()) { + var idx = INDEX_PAGE.cloneNode(true); + os.write(idx.getOuterHTML().getBytes(StandardCharsets.UTF_8)); } } } // see https://github.com/alfio-event/alf.io/issues/708 // use ngrok to test the preview - private byte[] getOpenGraphPage(InputStream is, String eventShortName, ServletWebRequest request, String lang) { + private byte[] getOpenGraphPage(Document eventOpenGraph, String eventShortName, ServletWebRequest request, String lang) { var event = eventRepository.findByShortName(eventShortName); var locale = RequestUtils.getMatchingLocale(request, event); if (lang != null && event.getContentLanguages().stream().map(ContentLanguage::getLanguage).anyMatch(lang::equalsIgnoreCase)) { @@ -159,7 +177,6 @@ private byte[] getOpenGraphPage(InputStream is, String eventShortName, ServletWe var title = messageSourceManager.getMessageSourceForEvent(event).getMessage("event.get-your-ticket-for", new String[] {event.getDisplayName()}, locale); - var eventOpenGraph = new Parser().parse(new InputStreamReader(is, StandardCharsets.UTF_8)); var head = eventOpenGraph.getElementsByTagName("head").get(0); eventOpenGraph.getElementsByTagName("html").get(0).setAttribute("lang", locale.getLanguage()); @@ -253,7 +270,8 @@ public void getLoginPage(@RequestParam(value="failed", required = false) String try (var os = response.getOutputStream()) { response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); - addCspHeader(response); + var nonce = addCspHeader(response); + model.addAttribute("nonce", nonce); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/login.ms"), model.asMap(), os); } } @@ -284,14 +302,23 @@ public void adminHome(Model model, @Value("${alfio.version}") String version, Ht try (var os = response.getOutputStream()) { response.setContentType(TEXT_HTML_CHARSET_UTF_8); response.setCharacterEncoding(UTF_8); - addCspHeader(response); + var nonce = addCspHeader(response); + model.addAttribute("nonce", nonce); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/admin-index.ms"), model.asMap(), os); } } + private static String getNonce() { + var nonce = new byte[16]; //128 bit = 16 bytes + SECURE_RANDOM.nextBytes(nonce); + return Hex.encodeHexString(nonce); + } + + public String addCspHeader(HttpServletResponse response) { + + String nonce = getNonce(); - public void addCspHeader(HttpServletResponse response) { String reportUri = ""; var conf = configurationManager.getFor(List.of(ConfigurationKeys.SECURITY_CSP_REPORT_ENABLED, ConfigurationKeys.SECURITY_CSP_REPORT_URI), ConfigurationLevel.system()); @@ -301,8 +328,8 @@ public void addCspHeader(HttpServletResponse response) { reportUri = " report-uri " + conf.get(ConfigurationKeys.SECURITY_CSP_REPORT_URI).getValueOrDefault("/report-csp-violation"); } // - // http://www.html5rocks.com/en/tutorials/security/content-security-policy/ - // lockdown policy + // https://csp.withgoogle.com/docs/strict-csp.html + // with base-uri set to 'self' response.addHeader("Content-Security-Policy", "default-src 'none'; "//block all by default + " script-src 'self' https://js.stripe.com https://checkout.stripe.com/ https://m.stripe.network https://api.stripe.com/ https://ssl.google-analytics.com/ https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/api2/ https://maps.googleapis.com/;"// @@ -315,5 +342,7 @@ public void addCspHeader(HttpServletResponse response) { + " media-src blob: 'self';"//for loading camera api + " connect-src 'self' https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://maps.googleapis.com/ https://geocoder.cit.api.here.com;" //<- currently stripe.js use jsonp but if they switch to xmlhttprequest+cors we will be ready + reportUri); + + return nonce; } } diff --git a/src/main/resources/alfio/web-templates/admin-index.ms b/src/main/resources/alfio/web-templates/admin-index.ms index 3adcab684d..50a7384b87 100644 --- a/src/main/resources/alfio/web-templates/admin-index.ms +++ b/src/main/resources/alfio/web-templates/admin-index.ms @@ -17,94 +17,94 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + Alf.io Admin @@ -214,6 +214,6 @@ - + \ No newline at end of file diff --git a/src/main/resources/alfio/web-templates/login.ms b/src/main/resources/alfio/web-templates/login.ms index ba6078550d..10051e1b11 100644 --- a/src/main/resources/alfio/web-templates/login.ms +++ b/src/main/resources/alfio/web-templates/login.ms @@ -16,7 +16,7 @@ Authentication {{/demoModeEnabled}} {{#hasRecaptchaApiKey}} - + {{/hasRecaptchaApiKey}} @@ -65,6 +65,6 @@ - + \ No newline at end of file From f70a446020c4e7a68aff704865c3f0ba81a84e8a Mon Sep 17 00:00:00 2001 From: Sylvain Jermini Date: Thu, 5 Sep 2019 15:36:06 +0200 Subject: [PATCH 3/4] #740 update csp policy to strict dynamic --- .../java/alfio/controller/IndexController.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main/java/alfio/controller/IndexController.java b/src/main/java/alfio/controller/IndexController.java index 7233cdcd85..b670b87c57 100644 --- a/src/main/java/alfio/controller/IndexController.java +++ b/src/main/java/alfio/controller/IndexController.java @@ -159,6 +159,7 @@ public void replyToIndex(@PathVariable(value = "eventShortName", required = fals } else { try (var os = response.getOutputStream()) { var idx = INDEX_PAGE.cloneNode(true); + idx.getElementsByTagName("script").forEach(element -> element.setAttribute("nonce", nonce)); os.write(idx.getOuterHTML().getBytes(StandardCharsets.UTF_8)); } } @@ -331,16 +332,9 @@ public String addCspHeader(HttpServletResponse response) { // https://csp.withgoogle.com/docs/strict-csp.html // with base-uri set to 'self' - response.addHeader("Content-Security-Policy", "default-src 'none'; "//block all by default - + " script-src 'self' https://js.stripe.com https://checkout.stripe.com/ https://m.stripe.network https://api.stripe.com/ https://ssl.google-analytics.com/ https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/api2/ https://maps.googleapis.com/;"// - + " style-src 'self' 'unsafe-inline';" // unsafe-inline for style is acceptable... - + " img-src 'self' https: data:;"// - + " child-src 'self';" - + " worker-src 'self';"//webworker - + " frame-src 'self' https://js.stripe.com https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://www.google.com;" - + " font-src 'self';"// - + " media-src blob: 'self';"//for loading camera api - + " connect-src 'self' https://checkout.stripe.com https://m.stripe.network https://m.stripe.com https://maps.googleapis.com/ https://geocoder.cit.api.here.com;" //<- currently stripe.js use jsonp but if they switch to xmlhttprequest+cors we will be ready + response.addHeader("Content-Security-Policy", "object-src 'none'; "+ + "script-src 'nonce-" + nonce + "' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:; " + + "base-uri 'self'; " + reportUri); return nonce; From 05b56f7d818ebade3968ad175f0ea191c22b0955 Mon Sep 17 00:00:00 2001 From: Sylvain Jermini Date: Thu, 5 Sep 2019 15:51:56 +0200 Subject: [PATCH 4/4] #740 fix loading of opengraph template --- src/main/java/alfio/controller/IndexController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/alfio/controller/IndexController.java b/src/main/java/alfio/controller/IndexController.java index b670b87c57..15e8e68231 100644 --- a/src/main/java/alfio/controller/IndexController.java +++ b/src/main/java/alfio/controller/IndexController.java @@ -79,7 +79,7 @@ public class IndexController { static { try (var idxIs = new ClassPathResource("alfio-public-frontend-index.html").getInputStream(); - var idxOpenGraph = new ClassPathResource("alfio-public-frontend-index.html").getInputStream()) { + var idxOpenGraph = new ClassPathResource("alfio/web-templates/event-open-graph-page.html").getInputStream()) { var parser = new Parser(); INDEX_PAGE = parser.parse(new InputStreamReader(idxIs, StandardCharsets.UTF_8)); OPEN_GRAPH_PAGE = parser.parse(new InputStreamReader(idxOpenGraph, StandardCharsets.UTF_8));