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) { 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..15bda209a95 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,64 @@ 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); + // 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()); + 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"); + 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); + } + } + } } } }