VYPR
Medium severity4.3NVD Advisory· Published Jun 9, 2026· Updated Jun 9, 2026

CVE-2026-49848

CVE-2026-49848

Description

FreeSWITCH mod_verto allows pre-authentication userVariables injection via a race condition on WebSocket connections, impacting subsequent successful logins.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

FreeSWITCH mod_verto allows pre-authentication userVariables injection via a race condition on WebSocket connections, impacting subsequent successful logins.

Vulnerability

Prior to version 1.11.1, FreeSWITCH's mod_verto module, specifically the check_auth userauth branch, incorrectly wrote request-supplied userVariables into the connection state before comparing the provided password. Because these writes were append-only and the connection was not closed upon a failed password comparison, values supplied during a failed login attempt could persist on the same WebSocket and be carried into a subsequent successful login on that same connection.

Exploitation

An attacker with network access to the mod_verto WebSocket listener and valid credentials for an account on that listener can exploit this vulnerability. The attacker first sends a WebSocket request with a bad password and malicious userVariables. If authentication later succeeds on the same WebSocket connection, the userVariables from the failed attempt will be present alongside the variables from the successful login.

Impact

When authentication eventually succeeds on a compromised WebSocket connection, the connection's userVariables will contain a union of values from all prior failed attempts and the successful login. These combined values can influence channel variables on outbound verto.invite calls and inbound INVITEs targeting the session. This allows an authenticated user to manipulate call-side variables using data from earlier failed login attempts, rather than solely from the successful login frame.

Mitigation

This vulnerability has been patched in FreeSWITCH version 1.11.1, released on 2024-01-24 [2]. The fix restructures the userauth branch to ensure the password comparison occurs before any connection state is written, and userVariables are only applied upon successful authentication. As a workaround, restrict the verto WebSocket listener to trusted networks or disable mod_verto if it is not in use [1].

AI Insight generated on Jun 9, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

3
74d320834bd4

Merge commit from fork

https://github.com/signalwire/freeswitchDmitry VerenitsinMay 26, 2026Fixed in 1.11.1via llm-release-walk
1 file changed · +67 50
  • src/mod/endpoints/mod_verto/mod_verto.c+67 50 modified
    @@ -1057,7 +1057,7 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char *
     			if (jsock->profile->chop_domain && (domain = strchr(id, '@'))) {
     				*domain++ = '\0';
     			}
    -			
    +
     		}
     
     		if (jsock->profile->register_domain) {
    @@ -1087,27 +1087,10 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char *
     			}
     		}
     
    -
    -		if ((json_ptr = cJSON_GetObjectItem(params, "userVariables"))) {
    -			cJSON * i;
    -			
    -			switch_mutex_lock(jsock->flag_mutex);
    -			for(i = json_ptr->child; i; i = i->next) {
    -				if (i->type == cJSON_True) {
    -					switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "true");
    -				} else if (i->type == cJSON_False) {
    -					switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "false");
    -				} else if (!zstr(i->string) && !zstr(i->valuestring)) {
    -					switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, i->valuestring);
    -				}
    -			}
    -			switch_mutex_unlock(jsock->flag_mutex);
    -		}
    -
     		if (jsock->profile->send_passwd || verto_globals.send_passwd) {
     			switch_event_add_header_string(req_params, SWITCH_STACK_BOTTOM, "user_supplied_pass", passwd);
     		}
    -		
    +
     		switch_event_add_header_string(req_params, SWITCH_STACK_BOTTOM, "action", "jsonrpc-authenticate");
     
     		if (switch_xml_locate_user_merged("id", id, domain, NULL, &x_user, req_params) != SWITCH_STATUS_SUCCESS && !jsock->profile->blind_reg) {
    @@ -1120,20 +1103,8 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char *
     			const char *use_passwd = NULL, *verto_context = NULL, *verto_dialplan = NULL;
     			time_t now = switch_epoch_time_now(NULL);
     
    -			switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Login sucessful for user: %s domain: %s\n", id, domain);
    -			
    -			jsock->logintime = now;
    -			jsock->id = switch_core_strdup(jsock->pool, id);
    -			jsock->domain = switch_core_strdup(jsock->pool, domain);
    -			jsock->uid = switch_core_sprintf(jsock->pool, "%s@%s", id, domain);
    -			jsock->ready = 1;
    -
    -			if (!x_user) {
    -				switch_event_destroy(&req_params);
    -				r = SWITCH_TRUE;
    -				goto end;
    -			}
    -
    +			/* Pre-scan <user><params>: extract credentials and verto-context/dialplan
    +			 * into locals only. No jsock writes here. */
     			if ((x_params = switch_xml_child(x_user, "params"))) {
     				for (x_param = switch_xml_child(x_params, "param"); x_param; x_param = x_param->next) {
     					const char *var = switch_xml_attr_soft(x_param, "name");
    @@ -1155,8 +1126,63 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char *
     					} else if (!strcasecmp(var, "verto-dialplan")) {
     						verto_dialplan = val;
     					}
    +				}
    +			}
    +
    +			/* Password gate. blind_reg with no x_user passes by config. */
    +			if (x_user && (zstr(use_passwd) || strcmp(a1_hash ? a1_hash : passwd, use_passwd))) {
    +				*code = CODE_AUTH_FAILED;
    +				switch_snprintf(message, mlen, "Authentication Failure");
    +				login_fire_custom_event(jsock, params, 0, "Authentication Failure");
    +				switch_xml_clear_user_cache("id", id, domain);
    +				switch_xml_free(x_user);
    +				switch_event_destroy(&req_params);
    +				goto end;
    +			}
    +
    +			/* Commit jsock state — reachable only post-gate. */
    +			switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Login successful for user: %s domain: %s\n", id, domain);
    +
    +			jsock->logintime = now;
    +			jsock->id = switch_core_strdup(jsock->pool, id);
    +			jsock->domain = switch_core_strdup(jsock->pool, domain);
    +			jsock->uid = switch_core_sprintf(jsock->pool, "%s@%s", id, domain);
    +
    +			if ((json_ptr = cJSON_GetObjectItem(params, "userVariables"))) {
    +				cJSON *i;
    +
    +				switch_mutex_lock(jsock->flag_mutex);
    +				for (i = json_ptr->child; i; i = i->next) {
    +					if (i->type == cJSON_True) {
    +						switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "true");
    +					} else if (i->type == cJSON_False) {
    +						switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "false");
    +					} else if (!zstr(i->string) && !zstr(i->valuestring)) {
    +						switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, i->valuestring);
    +					}
    +				}
    +				switch_mutex_unlock(jsock->flag_mutex);
    +			}
     
    -					switch_event_add_header_string(jsock->params, SWITCH_STACK_BOTTOM, var, val);
    +			/* blind_reg path: no XML user located — jsock state already committed above;
    +			 * skip directory persistence (params/variables/dialplan/context) and return. */
    +			if (!x_user) {
    +				switch_event_destroy(&req_params);
    +				/* ready=1 is the last state write so cross-thread readers that
    +				 * gate on `ready && !zstr(uid)` see a fully populated jsock. */
    +				jsock->ready = 1;
    +				r = SWITCH_TRUE;
    +				goto end;
    +			}
    +
    +			/* Second pass over <user><params>: persist every entry into jsock->params.
    +			 * Pre-scan above only read credentials/verto-context/dialplan into locals.
    +			 * Must run post-gate — these headers feed channel variables on later calls. */
    +			if ((x_params = switch_xml_child(x_user, "params"))) {
    +				for (x_param = switch_xml_child(x_params, "param"); x_param; x_param = x_param->next) {
    +					switch_event_add_header_string(jsock->params, SWITCH_STACK_BOTTOM,
    +						switch_xml_attr_soft(x_param, "name"),
    +						switch_xml_attr_soft(x_param, "value"));
     				}
     			}
     
    @@ -1171,7 +1197,7 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char *
     					switch_mutex_unlock(jsock->flag_mutex);
     
     					switch_clear_flag(jsock, JPFLAG_AUTH_EXPIRED);
    -					
    +
     					if (!strcmp(var, "login-expires")) {
     						uint32_t tmp = atol(val);
     
    @@ -1194,21 +1220,12 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char *
     				jsock->context = switch_core_strdup(jsock->pool, verto_context);
     			}
     
    -
    -			if (!use_passwd || zstr(use_passwd) || strcmp(a1_hash ? a1_hash : passwd, use_passwd)) {
    -				r = SWITCH_FALSE;
    -				*code = CODE_AUTH_FAILED;
    -				switch_snprintf(message, mlen, "Authentication Failure");
    -				jsock->uid = NULL;
    -				login_fire_custom_event(jsock, params, 0, "Authentication Failure");
    -				switch_xml_clear_user_cache("id", id, domain);
    -			} else {
    -				switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"auth using %s\n",a1_hash ? "a1-hash" : "username & password");
    -				r = SWITCH_TRUE;
    -				check_permissions(jsock, x_user, params);
    -			}
    -
    -
    +			switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"auth using %s\n",a1_hash ? "a1-hash" : "username & password");
    +			check_permissions(jsock, x_user, params);
    +			/* ready=1 is the last state write so cross-thread readers that
    +			 * gate on `ready && !zstr(uid)` see a fully populated jsock. */
    +			jsock->ready = 1;
    +			r = SWITCH_TRUE;
     
     			switch_xml_free(x_user);
     		}
    
67b62fb969a6

Merge commit from fork

https://github.com/signalwire/freeswitchDmitry VerenitsinMay 26, 2026Fixed in 1.11.1via llm-release-walk
1 file changed · +14 3
  • src/mod/endpoints/mod_verto/mod_verto.c+14 3 modified
    @@ -43,6 +43,7 @@ SWITCH_MODULE_DEFINITION(mod_verto, mod_verto_load, mod_verto_shutdown, mod_vert
     #define HTTP_CHUNK_SIZE 1024 * 32
     #define HTTP_POST_MAX_BODY (10 * 1024 * 1024)   /* max accepted Content-Length for form-urlencoded POST */
     #define EP_NAME "verto.rtc"
    +#define VERTO_SPEED_TEST_MAX_SIZE (10 * 1024 * 1024)
     //#define WSS_STANDALONE 1
     #include "libks/ks.h"
     
    @@ -2112,24 +2113,34 @@ static void client_run(jsock_t *jsock)
     					char repl[2048] = "";
     					switch_time_t a, b;
     
    +					if (!switch_test_flag(jsock, JPFLAG_AUTHED)) {
    +						die("%s Speed-test request before authentication\n", jsock->name);
    +					}
    +
    +					if (bytes < 4) {
    +						continue;
    +					}
    +
     					if (s[1] == 'S' && s[2] == 'P') {
     
     						if (s[3] == 'U') {
    -							int i, size = 0;
    +							int i;
    +							long size;
     							char *p = s+4;
     							int loops = 0;
     							int rem = 0;
     							int dur = 0, j = 0;
     
    -							if ((size = atoi(p)) <= 0) {
    +							size = strtol(p, NULL, 10);
    +							if (size <= 0 || size > VERTO_SPEED_TEST_MAX_SIZE) {
     								continue;
     							}
     
     							a = switch_time_now();
     							do {
     								bytes = kws_read_frame(jsock->ws, &oc, &data);
     								s = (char *) data;
    -							} while (bytes && data && s[0] == '#' && s[3] == 'B');
    +							} while (bytes >= 4 && data && s[0] == '#' && s[3] == 'B');
     							b = switch_time_now();
     
     							if (!bytes || !data) continue;
    
19f881b67f35
https://github.com/signalwire/freeswitchFixed in 1.11.1via llm-release-walk

Vulnerability mechanics

Root cause

"The authentication check in mod_verto wrote user-supplied variables to the connection state before verifying the password, allowing them to persist across failed login attempts."

Attack vector

An unauthenticated attacker can send a WebSocket request to the mod_verto endpoint with a malformed password. This triggers a failed authentication attempt, but the request-supplied userVariables are written to the connection state. The same WebSocket connection can then be used for a subsequent, successful login, where the previously supplied userVariables will be present and potentially influence the session.

Affected code

The vulnerability resides in the `check_auth` function within `src/mod/endpoints/mod_verto/mod_verto.c`. Specifically, the code that reads and commits `userVariables` from the incoming JSON parameters to the `jsock` structure was moved to occur after the password comparison logic.

What the fix does

The patch restructures the `check_auth` function to perform a pre-scan of user variables and credentials into local variables before any writes to the connection state. The password comparison now occurs earlier in the process. Only after a successful password comparison are the user variables and other connection-specific data committed to the connection state, preventing the persistence of data from failed login attempts [patch_id=5390379].

Preconditions

  • authThe attacker does not need to be authenticated.
  • networkThe attacker must be able to establish a WebSocket connection to the FreeSWITCH server.

Generated on Jun 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.