The pre-patch call chain fires from an inbound SIP MESSAGE to a shell command in nine lines across two files, and the 1.11.3 fix for CVE-2026-116779 does not change any of those nine lines. The api chat interface is still registered in mod_dptools.c. The handler is still switch_api_execute, and system still bottoms out at popen(cmd, "r"). SignalWire's fix is a per-profile boolean named enable-chat-api-proto, shipped commented out in the four vanilla profiles. The GHSA advisory records the discovery mode as "exploitation has been observed in the wild," and CISA pinned the KEV remediation deadline to 2026-11-12.
CVE-2026-116779: FreeSWITCH Shipped the api Chat Proto. 1.11.3 Shipped the Checkbox.
patterns
cve
proof of concept
The chat layer dispatches by case-insensitive name lookup
Inbound SIP MESSAGE lands on sofia_presence_handle_sip_i_message(). On a stock profile, chat is on by default; sofia.c line 4656 of the pre-patch tree runs sofia_set_pflag(profile, PFLAG_ENABLE_CHAT) during profile construction. Both vanilla profiles (internal.xml, external.xml) hit that path.
The pre-patch dispatcher reads the To user and splits it on the first plus sign:
/* src/mod/endpoints/mod_sofia/sofia_presence.c, pre-patch */
char proto[512] = SOFIA_CHAT_PROTO;
...
if ((p = strchr(to_user, '+')) && p != to_user && strlen(to_user) < sizeof(proto)) {
switch_copy_string(proto, to_user, sizeof(proto));
p = strchr(proto, '+');
*p++ = '\0';
...
}The left half of the plus becomes proto. The right half becomes the address. There is no allow-list on proto. There is a comment above the block explaining the 512-byte truncation guard, and nothing about which protocol names are legal. A hundred and forty lines later, the dispatcher commits:
/* src/mod/endpoints/mod_sofia/sofia_presence.c:5021, pre-patch */
switch_core_chat_send(proto, event);switch_core_chat_send looks proto up in the chat interface hash. That hash is built with switch_core_hash_init_nocase(), which the fix commit itself notes: "The proto compare is case-insensitive, matching the chat interface registry." The lookup accepts whatever name is in the hash, in whatever case.
One of the names in the hash is api.
The api chat interface's handler is switch_api_execute
mod_dptools registers two chat interfaces at module load. The registrations are three lines apart:
/* src/mod/applications/mod_dptools/mod_dptools.c:6617-6618 */
SWITCH_ADD_CHAT(chat_interface, "event", event_chat_send);
SWITCH_ADD_CHAT(chat_interface, "api", api_chat_send);api_chat_send's body is a URL decode, a space split, and a call into the FreeSWITCH API dispatcher:
/* src/mod/applications/mod_dptools/mod_dptools.c:4669-4707 */
static switch_status_t api_chat_send(switch_event_t *message_event)
{
...
if (to) {
char *v = NULL;
switch_stream_handle_t stream = { 0 };
char *cmd = NULL, *arg;
if (!(v = switch_core_get_variable_dup(to))) {
v = strdup(to);
}
cmd = v;
switch_assert(cmd);
switch_url_decode(cmd);
if ((arg = strchr(cmd, ' '))) {
*arg++ = '\0';
}
SWITCH_STANDARD_STREAM(stream);
switch_api_execute(cmd, arg, NULL, &stream);
...
}
return SWITCH_STATUS_SUCCESS;
}switch_api_execute is the public dispatcher for every SWITCH_ADD_API registration in the FreeSWITCH tree. One of those registrations is system:
/* src/mod/applications/mod_commands/mod_commands.c:7633 */
SWITCH_ADD_API(commands_api_interface, "system", "Execute a system command",
system_function, SYSTEM_SYNTAX);system_function calls switch_stream_system(cmd, stream). switch_stream_system is popen:
/* src/switch_core.c:3545-3576 */
SWITCH_DECLARE(int) switch_stream_system(const char *cmd, switch_stream_handle_t *stream)
{
...
FILE* pipe = popen(cmd, "r");
if (!pipe) return 1;
...
}The nine-line call chain from an attacker-controlled To: user to /bin/sh -c is: sofia_presence.c strchr, sofia_presence.c switch_copy_string, sofia_presence.c switch_core_chat_send, hash lookup, mod_dptools.c switch_url_decode, mod_dptools.c switch_api_execute, mod_commands.c system_function, switch_core.c switch_stream_system, libc popen. Four of those lines are in files SignalWire did not touch. Five are in the one file the patch did touch.
The PoC that confirmed it runs as a single SIP MESSAGE with no body, from an authenticated digest session on port 5060:
MESSAGE sip:localhost SIP/2.0
To: <sip:api+system%20id@target>
From: <sip:1000@target>
...The daemon answers 202, enqueues the chat event, and the worker thread executes id inside the running FreeSWITCH process. popen returns stdout to the stream; the chat layer wraps it in an outbound MESSAGE and sends it back to the caller. Command execution and output exfiltration in one SIP round-trip, with the caller's digest authentication as the only gate.
The patch is 38 lines and the sink is not one of them
Commit 7015f6a7 is dated 2026-08-26 and signed by Dmitry Verenitsin. It modifies eight files. The diff is reproduced here in full, abbreviated at the context lines:
--- a/src/mod/endpoints/mod_sofia/sofia_presence.c
+++ b/src/mod/endpoints/mod_sofia/sofia_presence.c
@@ -4879,6 +4879,24 @@ void sofia_presence_handle_sip_i_message(...)
p = strchr(proto, '+');
*p++ = '\0';
+ if (!strcasecmp(proto, "api") && !sofia_test_pflag(profile, PFLAG_ENABLE_CHAT_API_PROTO)) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
+ "Profile [%s] rejected a MESSAGE from %s@%s (%s:%d) addressed to the 'api' chat proto [%s]. "
+ "Set enable-chat-api-proto=true on the profile to permit it.\n",
+ profile->name, switch_str_nil(from_user), switch_str_nil(from_host), network_ip, network_port, p);
+
+ nua_respond(nh, SIP_403_FORBIDDEN, NUTAG_WITH_THIS_MSG(de->data->e_msg), TAG_END());
+ ...
+ return;
+ }
+
if ((to_addr = strdup(p))) {
if ((p = strchr(to_addr, '+'))) {
*p = '@';The sofia.c side adds the XML-param-to-pflag wiring (six lines). mod_sofia.h adds one enum value, PFLAG_ENABLE_CHAT_API_PROTO. The remaining fourteen lines of the diff are XML comments in four vanilla profiles and the mod_sofia sample sofia.conf.xml:
<!-- lets an inbound MESSAGE to api+<command> run FreeSWITCH API commands, so enable only for trusted peers -->
<!-- <param name="enable-chat-api-proto" value="true"/> -->Count the files the patch did not touch. mod_dptools.c, where the api chat interface is registered and where its handler calls switch_api_execute: unchanged. mod_commands.c, where system is registered as an API and where it bottoms out in popen: unchanged. switch_core.c, where switch_stream_system calls popen: unchanged. The chat registry's case-insensitive lookup: unchanged. The default state of PFLAG_ENABLE_CHAT in profile construction: unchanged; stock profiles still process inbound chat, they now 403 on the api proto specifically.
A vanilla 1.11.3 profile renders the new param like this:
<!-- lets an inbound MESSAGE to api+<command> run FreeSWITCH API commands, so enable only for trusted peers -->
<!-- <param name="enable-chat-api-proto" value="true"/> -->Two XML comment tokens. Delete <!-- and --> from the second line and the parser reads an active <param> element whose value is true. The 403 branch does not fire. The pre-patch code path runs. The api chat interface answers. switch_api_execute runs. popen runs.
This is Default Is The Mitigation: the patched binary contains the unpatched primitive, and the toggle that gates it is one XML comment away. The pattern's other exhibits move a numeric constant (vLLM's CVE-2026-5497 raised the frame cap from unbounded to 32; DBI's CVE-2026-10879 raised a strlen * 3 buffer to strlen * 6 + 16) or flip a vendor boolean from false to true inside the same configuration XML the operator already edits (SAP Visual Composer's CVE-2026-89110 moved SecurityMode from false to the documented true). CVE-2026-116779 is the pattern at the SIP-protocol scale: the sink is a SIP MESSAGE, the toggle is an XML comment pair in /etc/freeswitch/sip_profiles/internal.xml, and the operator who reads the vendor comment "enable only for trusted peers" and uncomments the line to permit their own SIP peers to use the api chat interface has shipped the pre-patch binary.
The vendor comment labels the opt-in with a design judgment: enable it for trusted peers. SignalWire did not retire the api chat proto. They retitled it. The feature stays. It is now documented, and the documentation is a six-word vote of confidence that the design is legitimate for a sufficiently trusted peer set.
PR:N is correct because the stock directory ships the credentials
The CVSS vector is AV:N/AC:L/PR:N/UI:N. Privileges Required: None. The GHSA advisory qualifies this: "any authenticated profile account is sufficient; stock directory users without password material can also remove that barrier in affected configurations."
The stock directory ships with password material. conf/vanilla/directory/default/ contains twenty-four user XML files; 1000.xml through 1019.xml all declare:
<user id="1000">
<params>
<param name="password" value="$${default_password}"/>
...
</params>
...
</user>conf/vanilla/vars.xml defines the variable:
<!-- YOU SHOULD CHANGE THIS default_password value if you don't want to be subject to any
attack from random SIP peers... -->
<X-PRE-PROCESS cmd="set" data="default_password=1234"/>The vendor-shipped password for twenty users on every vanilla FreeSWITCH install is the literal string 1234. The variable name declares the role; the vars comment asks the operator to change it; the stock directory depends on it; the authenticated profile account requirement that the advisory cites is satisfied by digesting as 1000:1234.
PR:N is correct not because the mechanism bypasses authentication, but because the vendor shipped the authentication. The call-center operator who downloaded the FreeSWITCH getting-started image in 2024, opened port 5060 to the SBC, and forgot to prune extensions 1000 through 1019 is the authenticated profile account the advisory's qualifying clause names. The telecom reseller who provisioned a thousand branch PBXes from the same stock image and forgot it a thousand times is the thousand authenticated profile accounts the mass-exploitation campaigns are digesting against.
The chat-API proto is case-insensitive. The digest challenge is standard. The To: header does the rest.
The advisory publishes 63 days after the fix
The fix commit lands on master on 2026-08-26. The GHSA advisory publishes on 2026-10-28, sixty-three days later, credited to Örn Arnarson (Reporter) and Gunnlaugur (Analyst). NVD publishes on 2026-11-05. CISA adds the KEV entry with a seven-day remediation deadline of 2026-11-12. The advisory records the discovery mode in the same paragraph as the mechanism: "Exploitation has been observed in the wild."
The pipeline this timeline describes is Disclosure After Exploitation. The fix is upstream for sixty-three days as a public commit titled [mod_sofia] Add enable-chat-api-proto to gate the api chat proto before the advisory names it a vulnerability. Any operator watching the master branch could have read the commit message, read the comment in the vanilla profile XML, and reproduced the pre-patch primitive with a single MESSAGE the day the commit landed. The advisory acknowledges that someone did. The public disclosure is documentation of a thing that was already happening.
The twelve-day window between NVD publication and the KEV deadline is not a patch window. It is a window to inventory which stock profiles on which appliances have which commented-out values. The operators who uncommented <param name="enable-chat-api-proto" value="true"/> because the vendor comment told them it was fine for trusted peers are inside the patched version and outside the mitigation. The scan signature for 1.11.3-but-vulnerable is identical to the scan signature for 1.11.2-but-patched-at-the-config-layer: send an api+system%20id MESSAGE and read the 403 or the 202.
This was always chat routing
The CVE calls this a command injection. CWE-77 and CWE-78 both appear on the record. Neither word quite fits. There is no injection. The caller selects a chat protocol by string; one of the registered chat protocols is named api; its registered handler is the FreeSWITCH API dispatcher; one of the registered FreeSWITCH APIs is system; the system API's registered backend is popen. Every layer is a documented feature of the chat subsystem registering against the API subsystem registering against the shell. The exploit traverses five registrations. The patch gates one of them, conditionally, behind one boolean, defaulted off, documented as appropriate for trusted peers.
SignalWire did not fix the api chat proto. They shipped it as the product. The fix is the comment markers. Delete four characters and 1.11.3 is 1.11.2.
The fix is the comment markers. Delete four characters and 1.11.3 is 1.11.2.