diff --git a/query/api-src/org/labkey/remoteapi/RemoteConnections.java b/query/api-src/org/labkey/remoteapi/RemoteConnections.java index e47f8f0bb15..8099413f035 100644 --- a/query/api-src/org/labkey/remoteapi/RemoteConnections.java +++ b/query/api-src/org/labkey/remoteapi/RemoteConnections.java @@ -92,9 +92,10 @@ public static boolean createOrEditRemoteConnection(RemoteConnectionForm remoteCo } // validate the url string and connection + URL urlObj; try { - URL urlObj = new URL(url); + urlObj = new URL(url); URLConnection conn = urlObj.openConnection(); conn.connect(); } @@ -155,6 +156,13 @@ public static boolean createOrEditRemoteConnection(RemoteConnectionForm remoteCo if (CONNECTION_KIND_QUERY.equals(connectionKind)) singleConnectionMap.put(RemoteConnections.FIELD_CONTAINER, folderPath); singleConnectionMap.save(); + + if (isCleartextHttpUrl(urlObj)) + { + // Log the host only — interpolating the full URL could leak credentials embedded in userinfo (e.g., http://user:pass@host/). + LOG.warn("Remote connection '{}' is configured with a cleartext http:// URL (host: {}). Credentials will be sent unencrypted. Use https:// instead.", newName, urlObj.getHost()); + } + return true; } @@ -164,6 +172,11 @@ public static String getBriefMessage(Throwable t) return t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage(); } + public static boolean isCleartextHttpUrl(@NotNull URL url) + { + return "http".equalsIgnoreCase(url.getProtocol()); + } + public static boolean deleteRemoteConnection(RemoteConnectionForm remoteConnectionForm, Container container) { String name = remoteConnectionForm.getConnectionName(); @@ -346,5 +359,14 @@ public void testGetBriefMessage() assertEquals("boom", getBriefMessage(new IOException("boom"))); assertEquals("IOException", getBriefMessage(new IOException())); } + + @Test + public void testIsCleartextHttpUrl() throws MalformedURLException + { + assertTrue("http:// must be detected as cleartext", isCleartextHttpUrl(new URL("http://example.com/labkey"))); + assertTrue("scheme match is case-insensitive", isCleartextHttpUrl(new URL("HTTP://example.com/labkey"))); + assertFalse("https:// is encrypted", isCleartextHttpUrl(new URL("https://example.com/labkey"))); + assertFalse("HTTPS:// is encrypted", isCleartextHttpUrl(new URL("HTTPS://example.com/labkey"))); + } } } diff --git a/query/src/org/labkey/query/view/createRemoteConnection.jsp b/query/src/org/labkey/query/view/createRemoteConnection.jsp index b7a1cb289c3..065046f78df 100644 --- a/query/src/org/labkey/query/view/createRemoteConnection.jsp +++ b/query/src/org/labkey/query/view/createRemoteConnection.jsp @@ -22,6 +22,8 @@ <%@ page import="org.labkey.api.view.JspView" %> <%@ page import="org.labkey.query.controllers.QueryController" %> <%@ page import="org.labkey.remoteapi.RemoteConnections" %> +<%@ page import="java.net.MalformedURLException" %> +<%@ page import="java.net.URL" %> <%@ page extends="org.labkey.api.jsp.FormPage" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> <% @@ -35,14 +37,32 @@ String connectionKind = remoteConnectionForm.getConnectionKind(); boolean editConnection = StringUtils.isNotEmpty(name); String nameToShow = editConnection ? name : remoteConnectionForm.getNewConnectionName(); + boolean usingCleartextHttp = false; + if (url != null) + { + try + { + usingCleartextHttp = RemoteConnections.isCleartextHttpUrl(new URL(url)); + } + catch (MalformedURLException ignored) + { + // Malformed URLs are surfaced by server-side validation in createOrEditRemoteConnection; suppress here. + } + } %>

<%=h(RemoteConnections.MANAGEMENT_PAGE_INSTRUCTIONS)%>

+<% if (usingCleartextHttp) { %> +

+ Warning: this connection uses an http:// URL. The configured user and password will be sent to the remote + server in cleartext on every ETL run. Use https:// so credentials are encrypted in transit. +

+<% } %>
diff --git a/query/src/org/labkey/query/view/manageRemoteConnections.jsp b/query/src/org/labkey/query/view/manageRemoteConnections.jsp index e5947d2a6df..2a76fcb8158 100644 --- a/query/src/org/labkey/query/view/manageRemoteConnections.jsp +++ b/query/src/org/labkey/query/view/manageRemoteConnections.jsp @@ -15,6 +15,7 @@ * limitations under the License. */ %> +<%@ page import="org.apache.commons.lang3.StringUtils" %> <%@ page import="org.labkey.api.data.Container" %> <%@ page import="org.labkey.api.security.permissions.AdminOperationsPermission" %> <%@ page import="org.labkey.api.settings.AppProps" %> @@ -22,6 +23,10 @@ <%@ page import="org.labkey.api.view.JspView" %> <%@ page import="org.labkey.query.controllers.QueryController.RemoteQueryConnectionUrls" %> <%@ page import="org.labkey.remoteapi.RemoteConnections" %> +<%@ page import="java.net.MalformedURLException" %> +<%@ page import="java.net.URL" %> +<%@ page import="java.util.ArrayList" %> +<%@ page import="java.util.List" %> <%@ page import="java.util.Map" %> <%@ page extends="org.labkey.api.jsp.FormPage" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> @@ -29,11 +34,41 @@ <% Container c = getContainer(); boolean hasAdminOpsPerm = c.hasPermission(getUser(), AdminOperationsPermission.class); + + Map connectionMap = ((JspView>) HttpView.currentView()).getModelBean(); + + List cleartextConnections = new ArrayList<>(); + if (connectionMap != null) + { + for (String connectionName : connectionMap.values()) + { + Map props = RemoteConnections.getRemoteConnection( + RemoteConnections.REMOTE_QUERY_CONNECTIONS_CATEGORY, connectionName, c); + String connUrl = props.get(RemoteConnections.FIELD_URL); + if (connUrl != null) + { + try + { + if (RemoteConnections.isCleartextHttpUrl(new URL(connUrl))) + cleartextConnections.add(connectionName); + } + catch (MalformedURLException ignored) + { + // Malformed URLs are surfaced by server-side validation in createOrEditRemoteConnection; suppress here. + } + } + } + } %> +<% if (!cleartextConnections.isEmpty()) { %> +

+ Warning: one or more remote connections use a cleartext http:// URL: <%=h(StringUtils.join(cleartextConnections, ", "))%>. Credentials are sent unencrypted on every ETL run. +

+<% } %> +
<% - Map connectionMap = ((JspView>) HttpView.currentView()).getModelBean(); if (connectionMap == null) { %>

EncryptionKey has not been specified in <%= h(AppProps.getInstance().getWebappConfigurationFilename()) %>, or its value no longer matches key previously in use.