From 938b5d39ca8e829d34d1c94c4fe170580caaba12 Mon Sep 17 00:00:00 2001 From: Carlos Afonso <50378106+carlosjepard@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:56:15 +0100 Subject: [PATCH] Added headers to response roda 6 (#3271) --- .../roda/wui/common/utils/RequestUtils.java | 2 +- .../java/org/roda/wui/config/RodaConfig.java | 33 ++++++++++++++ .../wui/filter/SecurityHeadersFilter.java | 44 +++++++++++++++++++ .../config/theme/cookies/cookieconsent.js | 33 ++++---------- 4 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 roda-ui/roda-wui/src/main/java/org/roda/wui/filter/SecurityHeadersFilter.java diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/common/utils/RequestUtils.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/common/utils/RequestUtils.java index 34c18e213a..7f23554e75 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/common/utils/RequestUtils.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/common/utils/RequestUtils.java @@ -13,7 +13,7 @@ public static RequestContext parseHTTPRequest(HttpServletRequest request) { RequestContext requestContext = new RequestContext(); // get user - User user = UserUtility.getApiUser(request); + User user = UserUtility.getUser(request, true); // get headers RequestHeaders headers = new RequestHeaders(); diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/config/RodaConfig.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/config/RodaConfig.java index 6a6b966834..30ac07ada7 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/config/RodaConfig.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/config/RodaConfig.java @@ -1,14 +1,19 @@ package org.roda.wui.config; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import org.apereo.cas.client.session.SingleSignOutHttpSessionListener; import org.roda.wui.filter.OnOffFilter; +import org.roda.wui.filter.SecurityHeadersFilter; import org.roda.wui.servlets.ContextListener; import org.roda.wui.servlets.RodaWuiServlet; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -168,6 +173,34 @@ public FilterRegistrationBean casWebAuthFilter() { return registrationBean; } + @Bean + public FilterRegistrationBean securityHeadersFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new SecurityHeadersFilter()); + registrationBean.addUrlPatterns("/*"); // Apply the filter to all requests + return registrationBean; + } + + @Bean + public ServletContextInitializer servletContextInitializer() { + return new ServletContextInitializer() { + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + servletContext.getSessionCookieConfig().setSecure(true); + servletContext.getSessionCookieConfig().setHttpOnly(true); + } + }; + } + + @Configuration(proxyBeanMethods = false) + public class SameSiteConfiguration { + @Bean + public CookieSameSiteSupplier applicationCookieSameSiteSupplier() { + return CookieSameSiteSupplier.ofStrict(); + } + } + @Configuration public static class DefaultView implements WebMvcConfigurer { diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/SecurityHeadersFilter.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/SecurityHeadersFilter.java new file mode 100644 index 0000000000..2d41849abb --- /dev/null +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/SecurityHeadersFilter.java @@ -0,0 +1,44 @@ +package org.roda.wui.filter; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +public class SecurityHeadersFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + + httpServletResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + httpServletResponse.setHeader("Content-Security-Policy", + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com " + + "https://www.google-analytics.com https://www.gstatic.com; style-src 'self' 'unsafe-inline'; " + + "img-src 'self'; font-src 'self';"); + httpServletResponse.setHeader("X-XSS-Protection", "1; mode=block"); + httpServletResponse.setHeader("X-Permitted-Cross-Domain-Policies", "none"); + httpServletResponse.setHeader("Feature-Policy", "camera 'none'; fullscreen 'self'; geolocation *; " + + "microphone 'self'"); + httpServletResponse.setHeader("X-Frame-Options", "SAMEORIGIN"); + httpServletResponse.setHeader("X-Content-Type-Options", "nosniff"); + httpServletResponse.setHeader("Referrer-Policy", "no-referrer"); + httpServletResponse.setHeader("Permissions-Policy", "geolocation=(self)"); + + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/roda-ui/roda-wui/src/main/resources/config/theme/cookies/cookieconsent.js b/roda-ui/roda-wui/src/main/resources/config/theme/cookies/cookieconsent.js index f4a3500eaa..b3e883683d 100644 --- a/roda-ui/roda-wui/src/main/resources/config/theme/cookies/cookieconsent.js +++ b/roda-ui/roda-wui/src/main/resources/config/theme/cookies/cookieconsent.js @@ -13,8 +13,8 @@ // Change cookie consent options on the fly. var OPTIONS_UPDATER = 'update_cookieconsent_options'; - // Name of cookie to be set when dismissed - var DISMISSED_COOKIE = 'cookieconsent_dismissed'; + // Key used to store consent status in localStorage + var DISMISSED_KEY = 'cookieconsent_dismissed'; // The path to built in themes // Note: Directly linking to a version on the CDN like this is horrible, but it's less horrible than people downloading the code @@ -22,7 +22,7 @@ var THEME_BUCKET_PATH = '//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/1.0.10/'; // No point going further if they've already dismissed. - if (document.cookie.indexOf(DISMISSED_COOKIE) > -1 || (window.navigator && window.navigator.CookiesOK)) { + if (localStorage.getItem(DISMISSED_KEY) || (window.navigator && window.navigator.CookiesOK)) { return; } @@ -93,25 +93,8 @@ return null; }, - setCookie: function (name, value, expiryDays, domain, path) { - expiryDays = expiryDays || 365; - - var exdate = new Date(); - exdate.setDate(exdate.getDate() + expiryDays); - - var cookie = [ - name + '=' + value, - 'expires=' + exdate.toUTCString(), - 'path=' + path || '/' - ]; - - if (domain) { - cookie.push( - 'domain=' + domain - ); - } - - document.cookie = cookie.join(';'); + setLocalStorage: function (key, value) { + localStorage.setItem(key, value); }, addEventListener: function (el, event, eventListener) { @@ -332,12 +315,12 @@ dismiss: function (evt) { evt.preventDefault && evt.preventDefault(); evt.returnValue = false; - this.setDismissedCookie(); + this.setDismissed(); this.container.removeChild(this.element); }, - setDismissedCookie: function () { - Util.setCookie(DISMISSED_COOKIE, 'yes', this.options.expiryDays, this.options.domain, this.options.path); + setDismissed: function () { + Util.setLocalStorage(DISMISSED_KEY, 'yes'); } };