From f9306767af315ecf640e5505e403f504d62a3c31 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Thu, 5 Nov 2026 14:46:02 +0100 Subject: [PATCH 1/3] ui: login session checked before proceding. --- ui/src/main.js | 18 +++++++++++++++++- ui/src/store/modules/user.js | 5 ++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ui/src/main.js b/ui/src/main.js index 7441f8010865..0abc3d0852ab 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -68,8 +68,23 @@ vueApp.use(backupUtilPlugin) vueApp.use(extensions) vueApp.use(directives) +// drop stored values that are not valid JSON (e.g. 'undefined'), as reading them throws +vueProps.$localStorage.keys().forEach(key => { + try { + vueProps.$localStorage.get(key) + } catch (e) { + console.warn('Removing unreadable local storage entry: ' + key) + vueProps.$localStorage.remove(key) + } +}) + +let configLoaded = false + const renderError = (err) => { console.error('Fatal error during app initialization: ', err) + const details = configLoaded + ? 'The application failed to start: ' + err + '. Check the browser console for more information.' + : 'Missing or malformed config.json. Please ensure the file is present, accessible, and contains valid JSON. Check the browser console for more information.' const ErrorComponent = { render: () => h( 'div', @@ -81,7 +96,7 @@ const renderError = (err) => { h('summary', { style: 'cursor: pointer;' }, 'Technical details'), h('pre', { style: 'text-align: left; display: inline-block; margin-top: 10px;' - }, 'Missing or malformed config.json. Please ensure the file is present, accessible, and contains valid JSON. Check the browser console for more information.') + }, details) ]) ] ) @@ -97,6 +112,7 @@ fetch('config.json?ts=' + Date.now()) return response.json() }) .then(async config => { + configLoaded = true vueProps.$config = config let baseUrl = config.apiBase if (config.multipleServer) { diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 4103ff920d63..87a48dd93967 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -64,7 +64,10 @@ function setLoginCookies (result) { } function processLoginResponse (commit, response) { - const result = response.loginresponse || {} + const result = response?.loginresponse || {} + if (!result.sessionkey) { + throw new Error('Login response not valid.') + } setLoginCookies(result) vueProps.$localStorage.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000) commit('SET_TOKEN', result.sessionkey) From f12389eb6eece296c433cd3b45433b98e8a2f9bd Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Thu, 5 Nov 2026 15:49:01 +0100 Subject: [PATCH 2/3] servlet: fix login NPE for users created on first login --- .../main/java/com/cloud/api/ApiServlet.java | 10 ++++++++ .../java/com/cloud/api/ApiServletTest.java | 25 +++++++++++++++++++ ui/src/main.js | 1 - 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index f5ca18449fe1..b541eccca1fe 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -313,6 +313,11 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp if (!DefaultForgotPasswordAPIAuthenticatorCmd.APINAME.equalsIgnoreCase(command) || StringUtils.isNotBlank(username)) { LOGGER.debug("Authentication failure: {}", e.getMessage()); } + } catch (RuntimeException e) { + if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGIN_API) { + invalidateHttpSession(session, "invalidating session after unexpected login failure"); + } + throw e; } if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) { @@ -426,6 +431,11 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp } catch (final Exception ex) { LOGGER.error("unknown exception writing api response", ex); auditTrailSb.append(" unknown exception writing api response"); + if (!resp.isCommitted()) { + final String serializedResponseText = apiServer.getSerializedApiError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Internal error processing the request", params, responseType); + HttpUtils.writeHttpResponse(resp, serializedResponseText, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseType, ApiServer.JSONContentType.value()); + } } finally { ACCESSLOGGER.info(auditTrailSb.toString()); if (LOGGER.isDebugEnabled()) { diff --git a/server/src/test/java/com/cloud/api/ApiServletTest.java b/server/src/test/java/com/cloud/api/ApiServletTest.java index 749533625551..a1a2099bd306 100644 --- a/server/src/test/java/com/cloud/api/ApiServletTest.java +++ b/server/src/test/java/com/cloud/api/ApiServletTest.java @@ -318,6 +318,31 @@ public void processRequestInContextLogin() throws UnknownHostException { Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class)); } + @SuppressWarnings("unchecked") + @Test + public void processRequestInContextLoginUnexpectedExceptionInvalidatesSessionAndReturnsError() throws UnknownHostException { + Mockito.when(request.getMethod()).thenReturn("POST"); + Mockito.when(request.getSession(false)).thenReturn(null); + Mockito.when(request.getSession(true)).thenReturn(session); + HashMap params = new HashMap(); + params.put(ApiConstants.COMMAND, new String[] { "login" }); + params.put(ApiConstants.USERNAME, new String[] { "TEST" }); + params.put(ApiConstants.PASSWORD, new String[] { "TEST-PWD" }); + Mockito.when(request.getParameterMap()).thenReturn(params); + Mockito.when(authenticator.getAPIType()).thenReturn(APIAuthenticationType.LOGIN_API); + Mockito.when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), + Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))) + .thenThrow(new NullPointerException("test")); + Mockito.when(apiServer.getSerializedApiError(Mockito.eq(HttpServletResponse.SC_INTERNAL_SERVER_ERROR), Mockito.anyString(), Mockito.anyMap(), Mockito.anyString())) + .thenReturn("{\"errorresponse\":{}}"); + + servlet.processRequestInContext(request, response); + + Mockito.verify(session).invalidate(); + Mockito.verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + Assert.assertEquals("{\"errorresponse\":{}}", responseWriter.toString()); + } + @Test public void getClientAddressWithXForwardedFor() throws UnknownHostException { setConfigValue("proxy.header.verify", "true"); diff --git a/ui/src/main.js b/ui/src/main.js index 0abc3d0852ab..9f6cbb5bba6f 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -68,7 +68,6 @@ vueApp.use(backupUtilPlugin) vueApp.use(extensions) vueApp.use(directives) -// drop stored values that are not valid JSON (e.g. 'undefined'), as reading them throws vueProps.$localStorage.keys().forEach(key => { try { vueProps.$localStorage.get(key) From 222494d2ec8f27ba9dadd47f9248b66e0c1d2a41 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Thu, 12 Nov 2026 17:10:17 +0100 Subject: [PATCH 3/3] test: cover non-login unexpected exception in ApiServlet --- .../java/com/cloud/api/ApiServletTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/src/test/java/com/cloud/api/ApiServletTest.java b/server/src/test/java/com/cloud/api/ApiServletTest.java index a1a2099bd306..b51d62afe18d 100644 --- a/server/src/test/java/com/cloud/api/ApiServletTest.java +++ b/server/src/test/java/com/cloud/api/ApiServletTest.java @@ -343,6 +343,28 @@ public void processRequestInContextLoginUnexpectedExceptionInvalidatesSessionAnd Assert.assertEquals("{\"errorresponse\":{}}", responseWriter.toString()); } + @SuppressWarnings("unchecked") + @Test + public void processRequestInContextNonLoginUnexpectedExceptionKeepsSessionAndReturnsError() throws UnknownHostException { + Mockito.when(request.getMethod()).thenReturn("POST"); + Mockito.when(request.getSession(Mockito.anyBoolean())).thenReturn(session); + HashMap params = new HashMap(); + params.put(ApiConstants.COMMAND, new String[] { "login" }); + Mockito.when(request.getParameterMap()).thenReturn(params); + Mockito.when(authenticator.getAPIType()).thenReturn(APIAuthenticationType.READONLY_API); + Mockito.when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), + Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))) + .thenThrow(new NullPointerException("test")); + Mockito.when(apiServer.getSerializedApiError(Mockito.eq(HttpServletResponse.SC_INTERNAL_SERVER_ERROR), Mockito.anyString(), Mockito.anyMap(), Mockito.anyString())) + .thenReturn("{\"errorresponse\":{}}"); + + servlet.processRequestInContext(request, response); + + Mockito.verify(session, Mockito.never()).invalidate(); + Mockito.verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + Assert.assertEquals("{\"errorresponse\":{}}", responseWriter.toString()); + } + @Test public void getClientAddressWithXForwardedFor() throws UnknownHostException { setConfigValue("proxy.header.verify", "true");