This is an automated email from the ASF dual-hosted git repository.

ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 5f919b428b Improved externalLoginKey handling for cross-application 
navigation (#1710)
5f919b428b is described below

commit 5f919b428ba43eff02e6be1712de30880f0bfc34
Author: Krishna Uprit <[email protected]>
AuthorDate: Mon Aug 24 11:24:30 2026 +0530

    Improved externalLoginKey handling for cross-application navigation (#1710)
    
    - External login keys are now single-use per webapp and time-limited,
    instead of a reusable token stored indefinitely in a JVM-wide map.
    - Key is no longer attached to inter-app link targets that carry a
    scheme or a network-path authority (widget links and tree links).
    - A failed base-permission/logout check is now treated as a no-op
    instead of proceeding with a null userLogin.
    - Key usage is tracked per webapp, so a key stays usable for the other
    webapps it was issued for.
    - Added unit test coverage for key reuse rejection and the inter-app
    absolute-target check; fixed an NPE in the replay test caused by an
    unstubbed getServletContext().
    
    Thank you Krishna Uprit for your contribution.
    
    ---------
    
    Co-authored-by: Ashish Vijaywargiya <[email protected]>
    Co-authored-by: Krishnauprit18 <[email protected]>
---
 .../webapp/control/ExternalLoginKeysManager.java   | 147 +++++++++++++++------
 .../control/ExternalLoginKeysManagerTests.java     |  78 +++++++++++
 .../java/org/apache/ofbiz/widget/WidgetWorker.java |  25 +++-
 .../widget/renderer/html/HtmlTreeRenderer.java     |   5 +-
 .../org/apache/ofbiz/widget/WidgetWorkerTest.java  |  41 ++++++
 5 files changed, 252 insertions(+), 44 deletions(-)

diff --git 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
index c3389144bd..46d64475b2 100644
--- 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
+++ 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
@@ -19,6 +19,7 @@
 package org.apache.ofbiz.webapp.control;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -41,12 +42,65 @@ import org.apache.ofbiz.webapp.WebAppUtil;
 public class ExternalLoginKeysManager {
     private static final String MODULE = 
ExternalLoginKeysManager.class.getName();
     private static final String EXTERNAL_LOGIN_KEY_ATTR = "externalLoginKey";
-    // This Map is keyed by the randomly generated externalLoginKey and the 
value is a UserLogin GenericValue object
-    private static final Map<String, GenericValue> EXTERNAL_LOGIN_KEYS = new 
ConcurrentHashMap<>();
+    // How long a minted key remains redeemable. Bounds the window during 
which a leaked key
+    // (address bar, browser history, Referer header, proxy log) is worth 
anything to an attacker.
+    private static final long EXTERNAL_LOGIN_KEY_TTL_MILLIS = 2 * 60 * 1000;
+    // This Map is keyed by the randomly generated externalLoginKey and the 
value is the ticket
+    // describing who it authenticates as and until when. One render mints one 
key and embeds it
+    // in every cross-webapp link on that page (see getExternalLoginKey's 
request-attribute
+    // cache), so a ticket is redeemable once per distinct destination webapp 
-- see
+    // ExternalLoginTicket#redeemFor -- rather than once globally: that keeps 
the ordinary
+    // app-switcher workflow (open two different apps from the same page) 
working while still
+    // rejecting a second redemption against the same webapp.
+    private static final Map<String, ExternalLoginTicket> EXTERNAL_LOGIN_KEYS 
= new ConcurrentHashMap<>();
 
     // This variable is set to empty so we know need to read from the 
properties file.
     private static String isExternalLoginKeyEnabled = "";
 
+    /**
+     * A minted external login key, bound to the UserLogin it authenticates 
and to a deadline.
+     * Optionally bound to a target context path too, for mint sites that know 
which webapp the
+     * key is destined for; existing mint sites do not, so that field is null 
and the check is
+     * skipped for them.
+     */
+    private static final class ExternalLoginTicket {
+        private final GenericValue userLogin;
+        private final String targetContextPath;
+        private final long expiresAtMillis;
+        // Context paths this ticket has already been redeemed for. A single 
render shares one
+        // key across every cross-webapp link it emits, so the same key 
legitimately needs to
+        // authenticate into several different webapps; this set is what stops 
it authenticating
+        // into the *same* webapp twice, which is the actual replay this 
ticket must prevent.
+        private final Set<String> redeemedContextPaths = 
ConcurrentHashMap.newKeySet();
+
+        ExternalLoginTicket(GenericValue userLogin, String targetContextPath) {
+            this.userLogin = userLogin;
+            this.targetContextPath = targetContextPath;
+            this.expiresAtMillis = System.currentTimeMillis() + 
EXTERNAL_LOGIN_KEY_TTL_MILLIS;
+        }
+
+        GenericValue getUserLogin() {
+            return userLogin;
+        }
+
+        boolean isValidFor(HttpServletRequest request) {
+            if (System.currentTimeMillis() > expiresAtMillis) {
+                return false;
+            }
+            return targetContextPath == null || 
targetContextPath.equals(request.getServletContext().getContextPath());
+        }
+
+        /**
+         * Atomically marks this ticket as redeemed for the request's webapp.
+         * @param request the request presenting this ticket's key
+         * @return true the first time this webapp redeems this ticket, false 
on any repeat
+         *     (replay) for the same webapp
+         */
+        boolean redeemFor(HttpServletRequest request) {
+            return 
redeemedContextPaths.add(request.getServletContext().getContextPath());
+        }
+    }
+
     /**
      * Gets (and creates if necessary) an authentication token to be used for 
an external login parameter.
      * When a new token is created, it is persisted in the web session and in 
the web request and map entry keyed by the
@@ -81,7 +135,7 @@ public class ExternalLoginKeysManager {
 
             request.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey);
             session.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey);
-            EXTERNAL_LOGIN_KEYS.put(externalKey, userLogin);
+            EXTERNAL_LOGIN_KEYS.put(externalKey, new 
ExternalLoginTicket(userLogin, null));
             return externalKey;
         }
     }
@@ -110,49 +164,62 @@ public class ExternalLoginKeysManager {
         String externalKey = request.getParameter(EXTERNAL_LOGIN_KEY_ATTR);
         if (externalKey == null) return "success";
 
-        GenericValue userLogin = EXTERNAL_LOGIN_KEYS.get(externalKey);
-        if (userLogin != null) {
-            //to check it's the right tenant
-            //in case username and password are the same in different tenants
-            Delegator delegator = (Delegator) 
request.getAttribute("delegator");
-            String oldDelegatorName = delegator.getDelegatorName();
-            if 
(!oldDelegatorName.equals(userLogin.getDelegator().getDelegatorName())) {
-                delegator = 
DelegatorFactory.getDelegator(userLogin.getDelegator().getDelegatorName());
-                LocalDispatcher dispatcher = 
WebAppUtil.makeWebappDispatcher(request.getServletContext(), delegator);
-                LoginWorker.setWebContextObjects(request, response, delegator, 
dispatcher);
-            }
-            // found userLogin, do the external login...
-
-            // if the user is already logged in and the login is different, 
logout the other user
-            HttpSession session = request.getSession();
-            GenericValue currentUserLogin = (GenericValue) 
session.getAttribute("userLogin");
-            if (currentUserLogin != null) {
-                if 
(currentUserLogin.getString("userLoginId").equals(userLogin.getString("userLoginId")))
 {
-                    // same user, just make sure the autoUserLogin is set to 
the same and that the client cookie has the correct userLoginId
-                    LoginWorker.autoLoginSet(request, response);
-                    // Same for the SecuredLoginId cookie
-                    LoginWorker.createSecuredLoginIdCookie(request, response);
-                    return "success";
-                }
-
-                // logout the current user and login the new user...
-                LoginWorker.logout(request, response);
-                // ignore the return value; even if the operation failed we 
want to set the new UserLogin
-            }
+        // Look up without removing: the same key is shared across every 
cross-webapp link one
+        // render emits, so it must stay valid for whichever *other* 
destination webapps the
+        // user still hasn't visited yet. redeemFor(), below, is what actually 
stops replay --
+        // it rejects a second redemption against a webapp this exact ticket 
already logged into.
+        ExternalLoginTicket ticket = EXTERNAL_LOGIN_KEYS.get(externalKey);
+        if (ticket == null || !ticket.isValidFor(request) || 
!ticket.redeemFor(request)) {
+            Debug.logWarning("Could not find a valid, not-yet-redeemed 
userLogin for external login key: " + externalKey, MODULE);
+            // make sure the autoUserLogin is set to the same and that the 
client cookie has the correct userLoginId
+            LoginWorker.autoLoginSet(request, response);
+            return "success";
+        }
 
-            // check userLogin base permission and if it is enabled
-            request.getSession().setAttribute("userLogin", userLogin);
-            userLogin = LoginWorker.checkLogout(request, response);
+        GenericValue userLogin = ticket.getUserLogin();
+        //to check it's the right tenant
+        //in case username and password are the same in different tenants
+        Delegator delegator = (Delegator) request.getAttribute("delegator");
+        String oldDelegatorName = delegator.getDelegatorName();
+        if 
(!oldDelegatorName.equals(userLogin.getDelegator().getDelegatorName())) {
+            delegator = 
DelegatorFactory.getDelegator(userLogin.getDelegator().getDelegatorName());
+            LocalDispatcher dispatcher = 
WebAppUtil.makeWebappDispatcher(request.getServletContext(), delegator);
+            LoginWorker.setWebContextObjects(request, response, delegator, 
dispatcher);
+        }
+        // found userLogin, do the external login...
 
-            LoginWorker.doBasicLogin(userLogin, request, response);
+        // if the user is already logged in and the login is different, logout 
the other user
+        HttpSession session = request.getSession();
+        GenericValue currentUserLogin = (GenericValue) 
session.getAttribute("userLogin");
+        if (currentUserLogin != null) {
+            if 
(currentUserLogin.getString("userLoginId").equals(userLogin.getString("userLoginId")))
 {
+                // same user, just make sure the autoUserLogin is set to the 
same and that the client cookie has the correct userLoginId
+                LoginWorker.autoLoginSet(request, response);
+                // Same for the SecuredLoginId cookie
+                LoginWorker.createSecuredLoginIdCookie(request, response);
+                return "success";
+            }
 
-            // Create a secured cookie with the correct userLoginId
-            LoginWorker.createSecuredLoginIdCookie(request, response);
+            // logout the current user and login the new user...
+            LoginWorker.logout(request, response);
+            // ignore the return value; even if the operation failed we want 
to set the new UserLogin
+        }
 
-        } else {
-            Debug.logWarning("Could not find userLogin for external login key: 
" + externalKey, MODULE);
+        // check userLogin base permission and if it is enabled
+        request.getSession().setAttribute("userLogin", userLogin);
+        userLogin = LoginWorker.checkLogout(request, response);
+        if (userLogin == null) {
+            // base permission check failed or the account is flagged 
logged-out; checkLogout
+            // already tore the session login down, so stop here rather than 
dereference null.
+            LoginWorker.autoLoginSet(request, response);
+            return "success";
         }
 
+        LoginWorker.doBasicLogin(userLogin, request, response);
+
+        // Create a secured cookie with the correct userLoginId
+        LoginWorker.createSecuredLoginIdCookie(request, response);
+
         // make sure the autoUserLogin is set to the same and that the client 
cookie has the correct userLoginId
         LoginWorker.autoLoginSet(request, response);
         return "success";
diff --git 
a/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
 
b/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
index c5c3c7b1f9..7976bcb669 100644
--- 
a/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
+++ 
b/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
@@ -20,15 +20,23 @@ package org.apache.ofbiz.webapp.control;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import jakarta.servlet.ServletContext;
 import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
 
+import org.apache.ofbiz.entity.Delegator;
 import org.apache.ofbiz.entity.GenericValue;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 
 public class ExternalLoginKeysManagerTests {
     @Test
@@ -79,4 +87,74 @@ public class ExternalLoginKeysManagerTests {
         verify(request).setAttribute("externalLoginKey", externalLoginKey);
         verify(session).setAttribute("externalLoginKey", externalLoginKey);
     }
+
+    @Test
+    public void checkExternalLoginKeyIgnoresUnknownKeyWithoutLoggingAnyoneIn() 
{
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        
when(request.getParameter("externalLoginKey")).thenReturn("ELunknown-key-not-in-map");
+
+        try (MockedStatic<LoginWorker> loginWorker = 
mockStatic(LoginWorker.class)) {
+            String result = 
ExternalLoginKeysManager.checkExternalLoginKey(request, response);
+
+            assertEquals("success", result);
+            loginWorker.verify(() -> LoginWorker.checkLogout(any(), any()), 
never());
+            loginWorker.verify(() -> LoginWorker.doBasicLogin(any(), any(), 
any()), never());
+            loginWorker.verify(() -> LoginWorker.autoLoginSet(request, 
response));
+        }
+    }
+
+    @Test
+    public void checkExternalLoginKeyConsumesTheKeySoItCannotBeReplayed() {
+        // Mint a key the way ScreenRenderer/RequestHandler do.
+        Delegator delegator = mock(Delegator.class);
+        when(delegator.getDelegatorName()).thenReturn("default");
+        GenericValue userLogin = mock(GenericValue.class);
+        when(userLogin.getDelegator()).thenReturn(delegator);
+        when(userLogin.getString("userLoginId")).thenReturn("demoadmin");
+
+        HttpServletRequest mintRequest = mock(HttpServletRequest.class);
+        HttpSession mintSession = mock(HttpSession.class);
+        when(mintRequest.getSession()).thenReturn(mintSession);
+        when(mintRequest.getAttribute("userLogin")).thenReturn(userLogin);
+        String key = ExternalLoginKeysManager.getExternalLoginKey(mintRequest);
+
+        // Both redemptions target the same webapp, so the second one is the 
actual replay case.
+        ServletContext servletContext = mock(ServletContext.class);
+        when(servletContext.getContextPath()).thenReturn("/partymgr");
+
+        try (MockedStatic<LoginWorker> loginWorker = 
mockStatic(LoginWorker.class)) {
+            loginWorker.when(() -> LoginWorker.checkLogout(any(), 
any())).thenReturn(userLogin);
+
+            // First redemption: a cookie-less client presents the freshly 
minted key.
+            HttpServletRequest firstUse = mock(HttpServletRequest.class);
+            HttpServletResponse firstResponse = 
mock(HttpServletResponse.class);
+            HttpSession firstSession = mock(HttpSession.class);
+            when(firstUse.getParameter("externalLoginKey")).thenReturn(key);
+            when(firstUse.getAttribute("delegator")).thenReturn(delegator);
+            when(firstUse.getSession()).thenReturn(firstSession);
+            when(firstUse.getServletContext()).thenReturn(servletContext);
+
+            String firstResult = 
ExternalLoginKeysManager.checkExternalLoginKey(firstUse, firstResponse);
+
+            assertEquals("success", firstResult);
+            loginWorker.verify(() -> LoginWorker.doBasicLogin(userLogin, 
firstUse, firstResponse), times(1));
+
+            // Replay: a second client presents the very same key value, 
against the same webapp.
+            HttpServletRequest replay = mock(HttpServletRequest.class);
+            HttpServletResponse replayResponse = 
mock(HttpServletResponse.class);
+            HttpSession replaySession = mock(HttpSession.class);
+            when(replay.getParameter("externalLoginKey")).thenReturn(key);
+            when(replay.getAttribute("delegator")).thenReturn(delegator);
+            when(replay.getSession()).thenReturn(replaySession);
+            when(replay.getServletContext()).thenReturn(servletContext);
+
+            String replayResult = 
ExternalLoginKeysManager.checkExternalLoginKey(replay, replayResponse);
+
+            assertEquals("success", replayResult);
+            // doBasicLogin was called exactly once overall: never for the 
replay.
+            loginWorker.verify(() -> LoginWorker.doBasicLogin(any(), any(), 
any()), times(1));
+            loginWorker.verify(() -> LoginWorker.autoLoginSet(replay, 
replayResponse));
+        }
+    }
 }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java
index 43cd182bd9..e32037954d 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java
@@ -48,6 +48,7 @@ import org.jsoup.parser.Tag;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.HashMap;
+import java.util.regex.Pattern;
 
 import static org.apache.ofbiz.base.util.UtilValidate.isNotEmpty;
 
@@ -55,8 +56,23 @@ public final class WidgetWorker {
 
     private static final String MODULE = WidgetWorker.class.getName();
 
+    // Matches a target that carries a scheme (e.g. "https:", "javascript:") 
or is protocol-relative
+    // ("//host/..."), meaning it points off the current host rather than at 
another control servlet.
+    private static final Pattern ABSOLUTE_TARGET = 
Pattern.compile("^(//|[A-Za-z][A-Za-z0-9+.-]*:)");
+
     private WidgetWorker() { }
 
+    /**
+     * Checks whether an inter-app link target is absolute (carries a scheme 
or a network-path
+     * authority) rather than a relative control path. Used to keep the 
externalLoginKey
+     * credential off targets that can point at an attacker-chosen host.
+     * @param target the widget-supplied target, already HTML-entity-decoded
+     * @return true if the target has a scheme or starts with {@code //}
+     */
+    public static boolean isAbsoluteTarget(String target) {
+        return target != null && ABSOLUTE_TARGET.matcher(target).find();
+    }
+
     public static URI buildHyperlinkUri(String target, String targetType, 
Map<String, String> parameterMap,
                                         String prefix, boolean fullPath, 
boolean secure, boolean encode,
                                         HttpServletRequest request, 
HttpServletResponse response) {
@@ -86,8 +102,13 @@ public final class WidgetWorker {
             }
         } else if ("inter-app".equals(targetType)) {
             uriString = localRequestName;
-            String externalLoginKey = (String) 
request.getAttribute("externalLoginKey");
-            additionalParameters.put("externalLoginKey", externalLoginKey);
+            // Never attach the credential to a target that can point at 
another host: the
+            // decoded target is checked here, not the original entity-encoded 
one, so an
+            // HTML-encoded scheme is caught too. See 
WidgetWorker#isAbsoluteTarget.
+            if (!isAbsoluteTarget(localRequestName)) {
+                String externalLoginKey = (String) 
request.getAttribute("externalLoginKey");
+                additionalParameters.put("externalLoginKey", externalLoginKey);
+            }
         } else if ("content".equals(targetType)) {
             uriString = getContentUrl(localRequestName, request);
         } else {
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
index cd9910a042..341e157f85 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
@@ -249,9 +249,10 @@ public class HtmlTreeRenderer extends HtmlWidgetRenderer 
implements TreeStringRe
                 newURL.append(target);
                 writer.append(newURL.toString());
             } else if ("inter-app".equalsIgnoreCase(urlMode) && req != null) {
+                writer.append(target);
                 String externalLoginKey = (String) 
req.getAttribute("externalLoginKey");
-                if (UtilValidate.isNotEmpty(externalLoginKey)) {
-                    writer.append(target);
+                // Never attach the credential to a target that can point at 
another host.
+                if (UtilValidate.isNotEmpty(externalLoginKey) && 
!WidgetWorker.isAbsoluteTarget(target)) {
                     if (target.contains("?")) {
                         writer.append("&externalLoginKey=");
                     } else {
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/WidgetWorkerTest.java 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/WidgetWorkerTest.java
index 05dfef9615..73979c4c4e 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/WidgetWorkerTest.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/WidgetWorkerTest.java
@@ -29,11 +29,16 @@ import java.net.URI;
 import java.util.HashMap;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasProperty;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
 
 public final class WidgetWorkerTest {
 
@@ -72,4 +77,40 @@ public final class WidgetWorkerTest {
         assertThat(withEncodedSpaces, hasProperty("scheme", 
equalTo("javascript")));
         assertThat(withEncodedSpaces, hasProperty("schemeSpecificPart", 
equalTo("set_value('system', 'system', '')")));
     }
+
+    @Test
+    public void interAppTargetKeepsExternalLoginKeyForARelativeControlPath() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        
when(request.getAttribute("externalLoginKey")).thenReturn("ELsome-key");
+
+        final URI uri = WidgetWorker.buildHyperlinkUri(
+                "/marketing/control/EditMarketingCampaign", "inter-app", new 
HashMap<>(), null,
+                false, true, true, request, null);
+
+        assertThat(uri.getQuery(), 
containsString("externalLoginKey=ELsome-key"));
+    }
+
+    @Test
+    public void interAppTargetDropsExternalLoginKeyForAnAbsoluteUrl() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        
when(request.getAttribute("externalLoginKey")).thenReturn("ELsome-key");
+
+        final URI uri = WidgetWorker.buildHyperlinkUri(
+                "https://attacker.example/collect";, "inter-app", new 
HashMap<>(), null,
+                false, true, true, request, null);
+
+        assertThat(uri.getQuery(), not(containsString("externalLoginKey")));
+    }
+
+    @Test
+    public void interAppTargetDropsExternalLoginKeyForAProtocolRelativeUrl() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        
when(request.getAttribute("externalLoginKey")).thenReturn("ELsome-key");
+
+        final URI uri = WidgetWorker.buildHyperlinkUri(
+                "//attacker.example/collect", "inter-app", new HashMap<>(), 
null,
+                false, true, true, request, null);
+
+        assertThat(uri.getQuery(), nullValue());
+    }
 }

Reply via email to