From 70ac4438b05136c54017d98a883bf6bf0f938d47 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 2 Oct 2022 15:45:49 +0200 Subject: feat(frontend): enable cross-origin isolation --- .../refinery/language/web/CacheControlFilter.java | 26 ++-------- .../language/web/SecurityHeadersFilter.java | 32 ++++++++++++ .../refinery/language/web/ServerLauncher.java | 57 +++++++++++----------- .../web/xtext/servlet/XtextWebSocketServlet.java | 27 +++++----- 4 files changed, 78 insertions(+), 64 deletions(-) create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java (limited to 'subprojects/language-web/src/main/java') diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java index 8ac8a21e..fbce62c1 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java @@ -1,21 +1,15 @@ package tools.refinery.language.web; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpHeader; + import java.io.IOException; import java.time.Duration; import java.util.Set; import java.util.regex.Pattern; -import org.eclipse.jetty.http.HttpHeader; - -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.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - public class CacheControlFilter implements Filter { private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2?)"); @@ -28,11 +22,6 @@ public class CacheControlFilter implements Filter { private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate"; - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing to initialize. - } - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -49,9 +38,4 @@ public class CacheControlFilter implements Filter { } chain.doFilter(request, response); } - - @Override - public void destroy() { - // Nothing to dispose. - } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java new file mode 100644 index 00000000..40dd7ee5 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java @@ -0,0 +1,32 @@ +package tools.refinery.language.web; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class SecurityHeadersFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { + if (response instanceof HttpServletResponse httpResponse) { + httpResponse.setHeader("Content-Security-Policy", "default-src 'self'; " + + // CodeMirror needs inline styles, see e.g., + // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2 + "style-src 'self' 'unsafe-inline'; " + + // Use 'data:' for displaying inline SVG backgrounds. + "img-src 'self' data:; " + + "object-src 'none'; " + + "base-uri 'none';"); + httpResponse.setHeader("X-Content-Type-Options", "nosniff"); + httpResponse.setHeader("X-Frame-Options", "DENY"); + httpResponse.setHeader("Referrer-Policy", "strict-origin"); + // Enable cross-origin isolation, https://web.dev/cross-origin-isolation-guide/ + httpResponse.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + httpResponse.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + // We do not expose any sensitive data over HTTP, so cross-origin is safe here. + httpResponse.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); + } + chain.doFilter(request, response); + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index ffd903d0..ffa61321 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -3,15 +3,8 @@ */ package tools.refinery.language.web; -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Set; - +import jakarta.servlet.DispatcherType; +import jakarta.servlet.SessionTrackingMode; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.DefaultServlet; @@ -21,11 +14,16 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import jakarta.servlet.DispatcherType; -import jakarta.servlet.SessionTrackingMode; import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.EnumSet; +import java.util.Set; + public class ServerLauncher { public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; @@ -36,24 +34,25 @@ public class ServerLauncher { public static final int HTTP_DEFAULT_PORT = 80; public static final int HTTPS_DEFAULT_PORT = 443; - - public static final String ALLOWED_ORIGINS_SEPARATOR = ";"; + + public static final String ALLOWED_ORIGINS_SEPARATOR = ","; private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class); private final Server server; - public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, Optional allowedOrigins) { + public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, String[] allowedOrigins) { server = new Server(bindAddress); var handler = new ServletContextHandler(); addSessionHandler(handler); addProblemServlet(handler, allowedOrigins); if (baseResource != null) { handler.setBaseResource(baseResource); - handler.setWelcomeFiles(new String[] { "index.html" }); + handler.setWelcomeFiles(new String[]{"index.html"}); addDefaultServlet(handler); } handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + handler.addFilter(SecurityHeadersFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); server.setHandler(handler); } @@ -63,13 +62,13 @@ public class ServerLauncher { handler.setSessionHandler(sessionHandler); } - private void addProblemServlet(ServletContextHandler handler, Optional allowedOrigins) { + private void addProblemServlet(ServletContextHandler handler, String[] allowedOrigins) { var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class); - if (allowedOrigins.isEmpty()) { + if (allowedOrigins == null) { LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!"); } else { var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR, - allowedOrigins.get()); + allowedOrigins); problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM, allowedOriginsString); } @@ -141,8 +140,8 @@ public class ServerLauncher { return Resource.newResource(webRootUri); } // Look for unpacked production artifacts (convenience for running from IDE). - var unpackedResourcePathComponents = new String[] { System.getProperty("user.dir"), "build", "webpack", - "production" }; + var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack", + "production"}; var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents)); if (unpackedResourceDir.isDirectory()) { return Resource.newResource(unpackedResourceDir); @@ -164,29 +163,31 @@ public class ServerLauncher { if (portStr != null) { return Integer.parseInt(portStr); } - return DEFAULT_LISTEN_PORT; + return DEFAULT_PUBLIC_PORT; } - private static Optional getAllowedOrigins() { + private static String[] getAllowedOrigins() { var allowedOrigins = System.getenv("ALLOWED_ORIGINS"); if (allowedOrigins != null) { - return Optional.of(allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR)); + return allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR); } return getAllowedOriginsFromPublicHostAndPort(); } - private static Optional getAllowedOriginsFromPublicHostAndPort() { + // This method returns null to indicate that all origins are allowed. + @SuppressWarnings("squid:S1168") + private static String[] getAllowedOriginsFromPublicHostAndPort() { var publicHost = getPublicHost(); if (publicHost == null) { - return Optional.empty(); + return null; } int publicPort = getPublicPort(); var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http"; var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort); if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) { var urlWithoutPort = String.format("%s://%s", scheme, publicHost); - return Optional.of(new String[] { urlWithPort, urlWithoutPort }); + return new String[]{urlWithPort, urlWithoutPort}; } - return Optional.of(new String[] { urlWithPort }); + return new String[]{urlWithPort}; } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java index 942ca380..a2ad2943 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java @@ -1,28 +1,25 @@ package tools.refinery.language.web.xtext.servlet; -import java.io.IOException; -import java.time.Duration; -import java.util.Set; - -import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; -import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; -import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; -import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import org.eclipse.jetty.websocket.server.*; import org.eclipse.xtext.resource.IResourceServiceProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; +import java.io.IOException; +import java.io.Serial; +import java.time.Duration; +import java.util.Set; public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implements JettyWebSocketCreator { - + @Serial private static final long serialVersionUID = -3772740838165122685L; - public static final String ALLOWED_ORIGINS_SEPARATOR = ";"; + public static final String ALLOWED_ORIGINS_SEPARATOR = ","; - public static final String ALLOWED_ORIGINS_INIT_PARAM = "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin"; + public static final String ALLOWED_ORIGINS_INIT_PARAM = + "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin"; public static final String XTEXT_SUBPROTOCOL_V1 = "tools.refinery.language.web.xtext.v1"; @@ -33,7 +30,7 @@ public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implem private static final Duration IDLE_TIMEOUT = Duration.ofSeconds(30); - private transient Logger log = LoggerFactory.getLogger(getClass()); + private final transient Logger log = LoggerFactory.getLogger(getClass()); private transient Set allowedOrigins = null; -- cgit v1.2.3-54-g00ecf