CVE-2026-49842
Description
FreeSWITCH mod_verto allows unauthenticated bandwidth amplification via a speed-test protocol, potentially causing DoS.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
FreeSWITCH mod_verto allows unauthenticated bandwidth amplification via a speed-test protocol, potentially causing DoS.
Vulnerability
FreeSWITCH versions prior to 1.11.1 contain a vulnerability in the mod_verto module. The WebSocket frame loop intercepts a speed-test protocol (#SPU, #SPB, #SPE) before any authentication checks. The declared payload size in the #SPU frame was parsed using atoi(), which only rejected non-positive values. This allowed an unauthenticated peer to request up to INT_MAX bytes, leading to a significant outbound bandwidth amplification when the server responded [1].
Exploitation
An attacker can exploit this vulnerability by sending a specially crafted WebSocket frame to the mod_verto listener. This frame initiates the speed-test protocol. No authentication, prior login, or user interaction is required. The attacker needs network reach to the mod_verto WebSocket listener, which typically runs on ports :8081 (plaintext) or :8082 (TLS). The vulnerability is present before the authentication gate, and TLS does not mitigate it as the vulnerable code path executes after transport termination [1].
Impact
Successful exploitation allows an unauthenticated attacker to cause the FreeSWITCH server to write tens of gigabytes of data back to the requester. This can saturate outbound link capacity and exhaust socket and thread resources, leading to a denial of service. Multiple concurrent requests can easily deny service to legitimate users on the same listener [1].
Mitigation
This vulnerability has been patched in FreeSWITCH version 1.11.1, released on 2023-03-15 [2]. The fix requires an authenticated session to reach the speed-test handler and caps the declared size to 10 MiB, preventing unbounded amplification [1]. Workarounds include restricting the mod_verto WebSocket listener to trusted networks via firewall or bind address, or disabling mod_verto entirely if 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
3(expand)+ 1 more
- (no CPE)
- (no CPE)range: <1.11.1
- Range: <1.11.1
Patches
267b62fb969a6Merge commit from fork
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;
33ee3663bbe6Merge commit from fork
5 files changed · +405 −4
src/mod/endpoints/mod_verto/mod_verto.c+5 −4 modified@@ -41,6 +41,7 @@ SWITCH_MODULE_RUNTIME_FUNCTION(mod_verto_runtime); SWITCH_MODULE_DEFINITION(mod_verto, mod_verto_load, mod_verto_shutdown, mod_verto_runtime); #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 WSS_STANDALONE 1 #include "libks/ks.h" @@ -1824,24 +1825,24 @@ static void http_run(jsock_t *jsock) char *buffer = NULL; switch_ssize_t len = 0, bytes = 0; - if (request->content_length && request->content_length > 10 * 1024 * 1024 - 1) { + if (request->content_length && request->content_length >= HTTP_POST_MAX_BODY) { char *data = "HTTP/1.1 413 Request Entity Too Large\r\n" "Content-Length: 0\r\n\r\n"; kws_raw_write(jsock->ws, data, strlen(data)); request->keepalive = 0; goto done; } - if (!(buffer = malloc(2 * 1024 * 1024))) { + if (!(buffer = malloc(request->content_length + 1))) { goto request_err; } while(bytes < (switch_ssize_t)request->content_length) { len = request->content_length - bytes; -#define WS_BLOCK 1 +#define WS_BLOCK 10000 /* ms; matches libks's internal default */ - if ((len = kws_raw_read(jsock->ws, buffer + bytes, len, WS_BLOCK)) < 0) { + if ((len = kws_raw_read(jsock->ws, buffer + bytes, len, WS_BLOCK)) <= 0) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Read error %" SWITCH_SSIZE_T_FMT"\n", len); goto done; }
tests/unit/conf_verto/freeswitch.xml+47 −0 added@@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<document type="freeswitch/xml"> + <X-PRE-PROCESS cmd="set" data="local_ip_v4=127.0.0.1"/> + <X-PRE-PROCESS cmd="set" data="domain=127.0.0.1"/> + + <section name="configuration" description="Configuration"> + + <configuration name="modules.conf" description="Modules"> + <modules> + <load module="mod_console"/> + <load module="mod_loopback"/> + <load module="mod_dptools"/> + <load module="mod_dialplan_xml"/> + <load module="mod_sndfile"/> + <load module="mod_verto"/> + </modules> + </configuration> + + <configuration name="switch.conf" description="Core Configuration"> + <settings> + <param name="colorize-console" value="false"/> + <param name="loglevel" value="debug"/> + <param name="rtp-start-port" value="16384"/> + <param name="rtp-end-port" value="16484"/> + </settings> + </configuration> + + <configuration name="console.conf" description="Console Logger"> + <mappings> + <map name="all" value="console,debug,info,notice,warning,err,crit,alert"/> + </mappings> + <settings> + <param name="colorize" value="false"/> + <param name="loglevel" value="debug"/> + </settings> + </configuration> + + <configuration name="timezones.conf" description="Timezones"> + <timezones> + <zone name="GMT" value="GMT0"/> + </timezones> + </configuration> + + <X-PRE-PROCESS cmd="include" data="verto.conf.xml"/> + + </section> +</document>
tests/unit/conf_verto/verto.conf.xml+36 −0 added@@ -0,0 +1,36 @@ +<configuration name="verto.conf" description="HTML5 Verto Endpoint (test)"> + + <settings> + <param name="debug" value="0"/> + </settings> + + <profiles> + <profile name="test-v4"> + <!-- + bind-local without "secure" → plain TCP. Port 33081 chosen to avoid + clashing with a default vanilla install on 8081. + --> + <param name="bind-local" value="127.0.0.1:33081"/> + <param name="force-register-domain" value="127.0.0.1"/> + <param name="userauth" value="true"/> + <param name="blind-reg" value="false"/> + <param name="rtp-ip" value="127.0.0.1"/> + <param name="timer-name" value="soft"/> + + <!-- + vhosts block is REQUIRED for http_run() to be invoked + (see mod_verto.c:2041,2061 — KWS_HTTP flag depends on it). + No auth-realm here, so requests bypass the 401 challenge and + reach the body-read path (which is itself pre-auth anyway). + --> + <vhosts> + <vhost domain="127.0.0.1"> + <param name="alias" value="localhost"/> + <param name="root" value="."/> + <param name="index" value="index.html"/> + </vhost> + </vhosts> + </profile> + </profiles> + +</configuration>
tests/unit/Makefile.am+1 −0 modified@@ -6,6 +6,7 @@ noinst_PROGRAMS += switch_core_video switch_core_db switch_vad switch_packetizer noinst_PROGRAMS += switch_stun noinst_PROGRAMS += test_tts_format noinst_PROGRAMS+= switch_hold switch_sip +noinst_PROGRAMS += test_mod_verto if HAVE_PCAP noinst_PROGRAMS += switch_rtp_pcap
tests/unit/test_mod_verto.c+316 −0 added@@ -0,0 +1,316 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2026, Anthony Minessale II <anthm@freeswitch.org> + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II <anthm@freeswitch.org> + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dmitry Verenitsin <dmitry.verenitsin@signalwire.com> + * + * + * test_mod_verto.c -- Tests for mod_verto + * + */ + +#include <switch.h> +#include <test/switch_test.h> + +#define VERTO_TEST_HOST "127.0.0.1" +#define VERTO_TEST_PORT 33081 + +/* Must match HTTP_POST_MAX_BODY in src/mod/endpoints/mod_verto/mod_verto.c */ +#define VERTO_POST_MAX_BODY (10 * 1024 * 1024) + +static switch_status_t verto_connect(switch_socket_t **sock_out, switch_memory_pool_t *pool) +{ + switch_sockaddr_t *addr = NULL; + switch_socket_t *sock = NULL; + int attempts; + + if (switch_sockaddr_info_get(&addr, VERTO_TEST_HOST, SWITCH_UNSPEC, + VERTO_TEST_PORT, 0, pool) != SWITCH_STATUS_SUCCESS) { + return SWITCH_STATUS_FALSE; + } + + for (attempts = 0; attempts < 50; attempts++) { + if (switch_socket_create(&sock, switch_sockaddr_get_family(addr), + SOCK_STREAM, SWITCH_PROTO_TCP, pool) != SWITCH_STATUS_SUCCESS) { + return SWITCH_STATUS_FALSE; + } + switch_socket_opt_set(sock, SWITCH_SO_TCP_NODELAY, 1); + + if (switch_socket_connect(sock, addr) == SWITCH_STATUS_SUCCESS) { + *sock_out = sock; + return SWITCH_STATUS_SUCCESS; + } + + switch_socket_close(sock); + sock = NULL; + switch_yield(100000); + } + + return SWITCH_STATUS_FALSE; +} + +static switch_status_t send_all(switch_socket_t *sock, const char *buf, switch_size_t len) +{ + switch_size_t remaining = len; + const char *p = buf; + + while (remaining > 0) { + switch_size_t n = remaining; + if (switch_socket_send(sock, p, &n) != SWITCH_STATUS_SUCCESS) { + return SWITCH_STATUS_FALSE; + } + if (n == 0) { + return SWITCH_STATUS_FALSE; + } + p += n; + remaining -= n; + } + return SWITCH_STATUS_SUCCESS; +} + +static switch_size_t read_status_line(switch_socket_t *sock, char *out, switch_size_t cap) +{ + switch_size_t got = 0; + + while (got < cap - 1) { + switch_size_t want = cap - 1 - got; + if (switch_socket_recv(sock, out + got, &want) != SWITCH_STATUS_SUCCESS || want == 0) { + break; + } + got += want; + if (memchr(out, '\n', got)) break; + } + out[got] = '\0'; + return got; +} + +FST_CORE_DB_BEGIN("./conf_verto") +{ + FST_SUITE_BEGIN(test_mod_verto) + { + FST_SETUP_BEGIN() + { + fst_requires_module("mod_verto"); + switch_yield(500000); + } + FST_SETUP_END() + + FST_TEARDOWN_BEGIN() + { + } + FST_TEARDOWN_END() + + FST_TEST_BEGIN(post_at_cap_returns_413) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %d\r\n" + "\r\n", + VERTO_POST_MAX_BODY); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send request"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 413"); + } while (0); + + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + + FST_TEST_BEGIN(post_small_body_parsed) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + const switch_size_t body_len = 32 * 1024; + char *body = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + body = malloc(body_len); + if (!body) { + fst_fail("could not allocate body buffer"); + break; + } + memset(body, 'x', body_len); + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %" SWITCH_SIZE_T_FMT "\r\n" + "\r\n", + body_len); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send headers"); + break; + } + if (send_all(sock, body, body_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send body"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 "); + fst_xcheck(strncmp(resp, "HTTP/1.1 413", 12) != 0, + "server returned 413 below cap"); + } while (0); + + free(body); + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + + FST_TEST_BEGIN(post_large_body_no_overflow) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + const switch_size_t body_len = 8 * 1024 * 1024; + char *body = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + body = malloc(body_len); + if (!body) { + fst_fail("could not allocate body buffer"); + break; + } + memset(body, 'x', body_len); + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %" SWITCH_SIZE_T_FMT "\r\n" + "\r\n", + body_len); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send headers"); + break; + } + if (send_all(sock, body, body_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send body"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 "); + fst_xcheck(strncmp(resp, "HTTP/1.1 413", 12) != 0, + "server returned 413 below cap"); + } while (0); + + free(body); + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + + FST_TEST_BEGIN(post_overflow_length_returns_413) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 9999999999\r\n" + "\r\n"); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send request"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 413"); + } while (0); + + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + } + FST_SUITE_END() +} +FST_CORE_END()
Vulnerability mechanics
Root cause
"The WebSocket frame loop in mod_verto processed speed-test protocol frames before authentication, allowing unauthenticated requests with excessively large payload sizes."
Attack vector
An unauthenticated peer can send a WebSocket frame with a '#'-prefixed speed-test protocol identifier (e.g., #SPU). The declared payload size in this frame is parsed using `atoi()`, which only rejects non-positive values. This allows an attacker to request an extremely large payload size, up to `INT_MAX` [patch_id=5390375]. The server then responds with a significantly amplified outbound bandwidth during the download phase.
Affected code
The vulnerability resides in the `client_run` function within `src/mod/endpoints/mod_verto/mod_verto.c`. Specifically, the code handling '#'-prefixed frames, including the parsing of payload size with `atoi()`, is affected.
What the fix does
The patch addresses the vulnerability by first gating the '#' speed-test protocol handling behind an authentication flag (`JPFLAG_AUTHED`) [patch_id=5390375]. Additionally, it replaces the vulnerable `atoi()` function with a bounded `strtol()` and caps the declared payload size at 10 MiB (`VERTO_SPEED_TEST_MAX_SIZE`). This prevents excessively large requests and mitigates the bandwidth amplification.
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
2News mentions
0No linked articles in our index yet.