From affa9f6e11219f8e4cddf33f87ba213373616497 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 14 May 2025 13:25:09 -0700 Subject: [PATCH 1/3] Enable a strong enforce CSP by default --- .../filters/ContentSecurityPolicyFilter.java | 1 + .../labkey/core/admin/AdminController.java | 73 ++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java b/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java index 787c1b3702d..f418d428d01 100644 --- a/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java +++ b/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java @@ -46,6 +46,7 @@ public class ContentSecurityPolicyFilter implements Filter { public static final String FEATURE_FLAG_DISABLE_ENFORCE_CSP = "disableEnforceCsp"; + public static final String FEATURE_FLAG_FORWARD_CSP_REPORTS = "forwardCspReports"; private static final String NONCE_SUBST = "REQUEST.SCRIPT.NONCE"; private static final String REPORT_PARAMETER_SUBSTITUTION = "CSP.REPORT.PARAMS"; diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index e2207f411f2..868621c02f1 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -361,11 +361,15 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.sql.SQLException; import java.text.DecimalFormat; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -11909,6 +11913,13 @@ public void addNavTrail(NavTree root) } } + private static final URI LABKEY_ORG_REPORT_ACTION; + + static + { + LABKEY_ORG_REPORT_ACTION = URI.create("https://www.labkey.org/admin-contentSecurityPolicyReport.api"); + } + @RequiresNoPermission @CSRF(CSRF.Method.NONE) public static class ContentSecurityPolicyReportAction extends ReadOnlyApiAction @@ -11955,19 +11966,61 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep String urlString = cspReport.optString("document-uri", null); if (urlString != null) { - String path = new URLHelper(urlString).deleteParameters().getPath(); + String path = new URLHelper(urlString).deleteParameters().getURIString(); if (null == reports.put(path, Boolean.TRUE) || _log.isDebugEnabled()) { - if (isNotBlank(userAgent)) - jsonObj.put("user-agent", userAgent); - String labkeyVersion = request.getParameter("labkeyVersion"); - if (null != labkeyVersion) - jsonObj.put("labkeyVersion", labkeyVersion); - String cspVersion = request.getParameter("cspVersion"); - if (null != cspVersion) - jsonObj.put("cspVersion", cspVersion); + // Forwarded reports already have user, ip, user-agent, etc. from the forwarding server + boolean forwarded = jsonObj.optBoolean("forwarded", false); + if (!forwarded) + { + jsonObj.put("user", getUser().getEmail()); + jsonObj.put("ip", request.getRemoteAddr()); + if (isNotBlank(userAgent)) + jsonObj.put("user-agent", userAgent); + String labkeyVersion = request.getParameter("labkeyVersion"); + if (null != labkeyVersion) + jsonObj.put("labkeyVersion", labkeyVersion); + String cspVersion = request.getParameter("cspVersion"); + if (null != cspVersion) + jsonObj.put("cspVersion", cspVersion); + } + var jsonStr = jsonObj.toString(2); - _log.warn("ContentSecurityPolicy warning on page: " + urlString + "\n" + jsonStr); + _log.warn("ContentSecurityPolicy warning on page: {}\n{}", urlString, jsonStr); + + if (!forwarded && OptionalFeatureService.get().isFeatureEnabled(ContentSecurityPolicyFilter.FEATURE_FLAG_FORWARD_CSP_REPORTS)) + { + jsonObj.put("forwarded", true); + + // Create an HttpClient + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + // Create the POST request + HttpRequest remoteRequest = HttpRequest.newBuilder() + .uri(LABKEY_ORG_REPORT_ACTION) + .header("Content-Type", request.getContentType()) // Use whatever the browser set + .POST(HttpRequest.BodyPublishers.ofString(jsonObj.toString(2))) + .build(); + + // Send the request and get the response + HttpResponse response = client.send(remoteRequest, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) + { + _log.error("ContentSecurityPolicy report forwarding to https://www.labkey.org failed: {}\n{}", response.statusCode(), response.body()); + } + else + { + JSONObject jsonResponse = new JSONObject(response.body()); + boolean success = jsonResponse.optBoolean("success", false); + if (!success) + { + _log.error("ContentSecurityPolicy report forwarding to https://www.labkey.org failed: {}", jsonResponse); + } + } + } } } } From 4a025c23b3d84d96ab02b83c65632e431ec91a5d Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 14 May 2025 13:47:23 -0700 Subject: [PATCH 2/3] Provide some guidance --- .../api/settings/OptionalFeatureStartupListener.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/src/org/labkey/api/settings/OptionalFeatureStartupListener.java b/api/src/org/labkey/api/settings/OptionalFeatureStartupListener.java index 90bff2eb0f5..cf42886ef2a 100644 --- a/api/src/org/labkey/api/settings/OptionalFeatureStartupListener.java +++ b/api/src/org/labkey/api/settings/OptionalFeatureStartupListener.java @@ -4,12 +4,15 @@ import org.apache.commons.lang3.BooleanUtils; import org.labkey.api.module.ModuleLoader; import org.labkey.api.settings.AdminConsole.OptionalFeatureFlag; +import org.labkey.api.util.DOM; import org.labkey.api.util.StartupListener; import java.util.Comparator; import java.util.Map; import static org.labkey.api.settings.AppProps.SCOPE_OPTIONAL_FEATURE; +import static org.labkey.api.util.DOM.SPAN; +import static org.labkey.api.util.DOM.STRONG; public class OptionalFeatureStartupListener implements StartupListener { @@ -38,6 +41,12 @@ public OptionalFeatureStartupPropertyHandler() ); } + @Override + public DOM.Renderable getScopeDescription() + { + return SPAN(STRONG(getScope()), " - set these properties to true/false to enable/disable the corresponding feature flag"); + } + @Override public void handle(Map properties) { From 39caab236ef9075772963d7b2b2d3f537048cc68 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Fri, 16 May 2025 11:35:30 -0700 Subject: [PATCH 3/3] Use X-FORWARDED-FOR if present --- core/src/org/labkey/core/admin/AdminController.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 868621c02f1..15bda209a95 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -11969,12 +11969,15 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep String path = new URLHelper(urlString).deleteParameters().getURIString(); if (null == reports.put(path, Boolean.TRUE) || _log.isDebugEnabled()) { - // Forwarded reports already have user, ip, user-agent, etc. from the forwarding server + // Don't modify forwarded reports; they already have user, ip, user-agent, etc. from the forwarding server. boolean forwarded = jsonObj.optBoolean("forwarded", false); if (!forwarded) { jsonObj.put("user", getUser().getEmail()); - jsonObj.put("ip", request.getRemoteAddr()); + String ipAddress = request.getHeader("X-FORWARDED-FOR"); + if (ipAddress == null) + ipAddress = request.getRemoteAddr(); + jsonObj.put("ip", ipAddress); if (isNotBlank(userAgent)) jsonObj.put("user-agent", userAgent); String labkeyVersion = request.getParameter("labkeyVersion");