Commit c1bb5c6ab3 for freeswitch.com
commit c1bb5c6ab35d948885e71e632c100889bc1b93ba
Author: Andrey Volk <andywolk@gmail.com>
Date: Wed Aug 5 18:44:33 2026 +0300
[Core, mod_commands] Interface allowlist (#3086)
* [core] Add interface allowlist to gate module app/api registration
Adds an optional, presence-activated allowlist in switch.conf.xml that
controls which modules may register application / api / json_api /
chat-application interfaces. With no <interface-allowlist> configured
nothing is enforced; when at least one <allow> entry is present, only
listed interfaces register at load time and all others are refused (the
module still loads and switch_loadable_module_process still returns
SUCCESS -- the blocked interface is simply never exposed).
Entries match at three levels of precision:
mod_commands - whole module
mod_commands.system - any interface named "system"
mod_commands.system.api - a specific type (app|api|json_api|chat_app)
Enforcement lives in switch_loadable_module_process() so every module, at
boot and at runtime `load`, is subject to the same policy. This gives
operators a way to disable the "system"/"spawn" shell-exec API commands
(and equivalents) system-wide.
Also adds the `interface_allowlist_dump [modules] [plain]` API, which
walks the loaded modules and prints their interfaces in the allowlist key
format so the current state can be captured and pruned offline into config.
* [mod_commands] Add tests for the interface allowlist
New test_interface_allowlist boots the core with an active
<interface-allowlist> (conf_interface_allowlist/) that permits only a
couple of mod_commands interfaces, then loads mod_commands and verifies:
- listed commands register and run (status, version) while unlisted and
shell-exec commands are refused (system, spawn, uptime) -- refusal
surfaces as switch_api_execute returning FALSE / command-not-found,
with the command function never invoked;
- a "module.name.type" entry gates by type: the API "status" loads while
the JSON API of the same name stays blocked;
- interface_allowlist_dump prints the config format in its xml, modules
and plain variants, and reflects module capabilities (system appears in
the dump even though it was blocked from registering).
* [config] Fix interior -- in interface-allowlist comment breaking XML parse
The explanatory comment used -- as em-dash pairs. The XML parser treats
-- inside a comment as the comment close, causing an "unclosed <!--"
error that prevents the whole freeswitch.xml from parsing (boot and
reloadxml both fail). Replace the -- pairs with ordinary punctuation.
* update .gitignore
* [core] Warn when interface-allowlist section is present but parses no entries
Co-authored-by: Chris Rienzo <chris@signalwire.com>
diff --git a/.gitignore b/.gitignore
index 2c92e39b59..1b9dcb1551 100644
--- a/.gitignore
+++ b/.gitignore
@@ -282,5 +282,6 @@ images/test_text.png
src/mod/codecs/mod_amrwb/test/test_amrwb
src/mod/endpoints/mod_sofia/test/sipp-based-tests
+src/mod/applications/mod_commands/test/test_interface_allowlist
.DS_Store
diff --git a/conf/vanilla/autoload_configs/switch.conf.xml b/conf/vanilla/autoload_configs/switch.conf.xml
index 29ac39976f..4c7cb9c0b2 100644
--- a/conf/vanilla/autoload_configs/switch.conf.xml
+++ b/conf/vanilla/autoload_configs/switch.conf.xml
@@ -208,4 +208,39 @@
</settings>
+ <!--
+ Interface allowlist (optional security hardening).
+
+ When this section contains at least one <allow> entry, ONLY the interfaces listed here
+ are permitted to register; every other application / api / json_api / chat-application
+ from ANY module is refused at load time (the module still loads, it just does not expose
+ the blocked interface). With this section absent or empty, nothing is enforced and all
+ interfaces load normally.
+
+ An interface is permitted if it matches ANY entry. Entries may be written at three
+ levels of precision:
+
+ <allow name="mod_commands"/> - permit every interface in mod_commands
+ <allow name="mod_commands.status"/> - permit any interface named "status" (all types)
+ <allow name="mod_commands.status.api"/> - permit ONLY the api named "status"
+
+ The optional trailing type token is one of: app, api, json_api, chat_app. This is how
+ you permit, e.g., the APP named "system" while still blocking an API/JSON-API of the
+ same name.
+
+ WARNING: this is an aggressive, system-wide allowlist. Once enabled you must list every
+ module (or interface) whose apps/APIs you rely on, including management commands from
+ mod_commands (load, unload, reload, status, ...) and your dialplan apps; otherwise the
+ system will not function. It only affects app/api/json_api/chat-app registration; codecs,
+ endpoints, timers, formats, etc. are never gated.
+ -->
+ <!--
+ <interface-allowlist>
+ <allow name="mod_commands"/>
+ <allow name="mod_dptools"/>
+ <allow name="mod_dialplan_xml"/>
+ <allow name="mod_sofia"/>
+ </interface-allowlist>
+ -->
+
</configuration>
diff --git a/src/include/switch_loadable_module.h b/src/include/switch_loadable_module.h
index 77e7b20e23..f72958aa58 100644
--- a/src/include/switch_loadable_module.h
+++ b/src/include/switch_loadable_module.h
@@ -156,6 +156,14 @@ SWITCH_DECLARE(switch_status_t) switch_loadable_module_enumerate_available(const
*/
SWITCH_DECLARE(switch_status_t) switch_loadable_module_enumerate_loaded(switch_modulename_callback_func_t callback, void *user_data);
+/*!
+ \brief Dump loaded module application/api/json_api/chat-application interfaces in interface-allowlist key format
+ \param stream stream to write the dump to
+ \param by_module if true emit one entry per module, otherwise one fully qualified entry (module.name.type) per interface
+ \param xml if true wrap entries as <allow/> tags inside an <interface-allowlist> element, otherwise emit raw keys
+ */
+SWITCH_DECLARE(void) switch_loadable_module_dump_interface_allowlist(switch_stream_handle_t *stream, switch_bool_t by_module, switch_bool_t xml);
+
/*!
\brief build a dynamic module object and register it (for use in double embeded modules)
\param filename the name of the modules source file
diff --git a/src/mod/applications/mod_commands/Makefile.am b/src/mod/applications/mod_commands/Makefile.am
index 18d5832063..7e2fc80924 100644
--- a/src/mod/applications/mod_commands/Makefile.am
+++ b/src/mod/applications/mod_commands/Makefile.am
@@ -12,8 +12,13 @@ libmodcommands_la_SOURCES = $(mod_commands_la_SOURCES)
libmodcommands_la_CFLAGS = $(mod_commands_la_CFLAGS)
noinst_PROGRAMS = test/test_mod_commands
+noinst_PROGRAMS += test/test_interface_allowlist
test_test_mod_commands_CFLAGS = $(SWITCH_AM_CFLAGS) -I../ -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\"
test_test_mod_commands_LDFLAGS = -avoid-version -no-undefined $(SWITCH_AM_LDFLAGS)
test_test_mod_commands_LDADD = libmodcommands.la $(switch_builddir)/libfreeswitch.la
+test_test_interface_allowlist_CFLAGS = $(SWITCH_AM_CFLAGS) -I../ -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\"
+test_test_interface_allowlist_LDFLAGS = -avoid-version -no-undefined $(SWITCH_AM_LDFLAGS)
+test_test_interface_allowlist_LDADD = libmodcommands.la $(switch_builddir)/libfreeswitch.la
+
TESTS = $(noinst_PROGRAMS)
diff --git a/src/mod/applications/mod_commands/mod_commands.c b/src/mod/applications/mod_commands/mod_commands.c
index b44035044a..a7f67c52d6 100644
--- a/src/mod/applications/mod_commands/mod_commands.c
+++ b/src/mod/applications/mod_commands/mod_commands.c
@@ -6665,6 +6665,26 @@ SWITCH_STANDARD_API(bg_spawn_function)
return SWITCH_STATUS_SUCCESS;
}
+#define INTERFACE_ALLOWLIST_DUMP_SYNTAX "[modules] [plain]"
+SWITCH_STANDARD_API(interface_allowlist_dump_function)
+{
+ switch_bool_t by_module = SWITCH_FALSE;
+ switch_bool_t xml = SWITCH_TRUE;
+
+ if (!zstr(cmd)) {
+ if (switch_stristr("module", cmd)) {
+ by_module = SWITCH_TRUE;
+ }
+ if (switch_stristr("plain", cmd) || switch_stristr("list", cmd)) {
+ xml = SWITCH_FALSE;
+ }
+ }
+
+ switch_loadable_module_dump_interface_allowlist(stream, by_module, xml);
+
+ return SWITCH_STATUS_SUCCESS;
+}
+
SWITCH_STANDARD_API(strftime_tz_api_function)
{
char *format = NULL;
@@ -7616,6 +7636,11 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_commands_load)
SWITCH_ADD_API(commands_api_interface, "spawn_stream", "Execute a spawn command and capture it's output", spawn_stream_function, SPAWN_SYNTAX);
}
+ SWITCH_ADD_API(commands_api_interface, "interface_allowlist_dump", "Dump loaded interfaces in interface-allowlist config format", interface_allowlist_dump_function, INTERFACE_ALLOWLIST_DUMP_SYNTAX);
+ switch_console_set_complete("add interface_allowlist_dump");
+ switch_console_set_complete("add interface_allowlist_dump modules");
+ switch_console_set_complete("add interface_allowlist_dump plain");
+
SWITCH_ADD_API(commands_api_interface, "acl", "Compare an ip to an acl list", acl_function, "<ip> <list_name>");
SWITCH_ADD_API(commands_api_interface, "alias", "Alias", alias_function, ALIAS_SYNTAX); SWITCH_ADD_API(commands_api_interface, "coalesce", "Return first nonempty parameter", coalesce_function, COALESCE_SYNTAX);
SWITCH_ADD_API(commands_api_interface, "banner", "Return the system banner", banner_function, "");
diff --git a/src/mod/applications/mod_commands/test/conf_interface_allowlist/freeswitch.xml b/src/mod/applications/mod_commands/test/conf_interface_allowlist/freeswitch.xml
new file mode 100644
index 0000000000..96a4be17a7
--- /dev/null
+++ b/src/mod/applications/mod_commands/test/conf_interface_allowlist/freeswitch.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<document type="freeswitch/xml">
+
+ <section name="configuration" description="Various Configuration">
+ <configuration name="modules.conf" description="Modules">
+ <modules>
+ <load module="mod_console"/>
+ <load module="mod_loopback"/>
+ <load module="mod_sndfile"/>
+ </modules>
+ </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="true"/>
+ <param name="loglevel" value="debug"/>
+ </settings>
+ </configuration>
+
+ <configuration name="switch.conf" description="Core Configuration">
+ <!--
+ Active interface allowlist used by the test. Because at least one <allow> entry
+ is present, enforcement is ON: only the interfaces below may register.
+ -->
+ <interface-allowlist>
+ <!-- Autoloaded helper modules permitted wholesale. -->
+ <allow name="mod_console"/>
+ <allow name="mod_loopback"/>
+ <allow name="mod_sndfile"/>
+
+ <!-- Type-qualified: only the API named "status" (the JSON API "status" stays blocked). -->
+ <allow name="mod_commands.status.api"/>
+ <!-- Name only: any type named "version". -->
+ <allow name="mod_commands.version"/>
+ <!-- Needed so the test can invoke the dump API itself. -->
+ <allow name="mod_commands.interface_allowlist_dump"/>
+ </interface-allowlist>
+ </configuration>
+
+ <configuration name="timezones.conf" description="Timezones">
+ <timezones>
+ <zone name="GMT" value="GMT0" />
+ </timezones>
+ </configuration>
+ </section>
+
+</document>
diff --git a/src/mod/applications/mod_commands/test/test_interface_allowlist.c b/src/mod/applications/mod_commands/test/test_interface_allowlist.c
new file mode 100644
index 0000000000..d36d29a12d
--- /dev/null
+++ b/src/mod/applications/mod_commands/test/test_interface_allowlist.c
@@ -0,0 +1,143 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2005-2024, 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
+ *
+ * Contributor(s):
+ * Chris Rienzo <chris@signalwire.com>
+ *
+ * test_interface_allowlist -- tests the switch.conf.xml <interface-allowlist>
+ *
+ * The core is booted with conf_interface_allowlist/freeswitch.xml, whose allowlist permits only
+ * a couple of mod_commands interfaces. mod_commands is then loaded and we verify that unlisted /
+ * dangerous commands are refused registration while listed ones load, that a "module.name.type"
+ * entry gates by interface type, and that interface_allowlist_dump prints the config format.
+ *
+ */
+
+#include <test/switch_test.h>
+#include <string.h>
+
+FST_CORE_BEGIN("conf_interface_allowlist")
+{
+ FST_MODULE_BEGIN(mod_commands, interface_allowlist_test)
+ {
+ FST_SETUP_BEGIN()
+ {
+ }
+ FST_SETUP_END()
+
+ /* Listed commands register and run; unlisted (including the shell-exec commands) are refused.
+ A refused command is not in the api hash, so switch_api_execute returns FALSE without ever
+ invoking the command function (empty arg means system/spawn would only print usage anyway). */
+ FST_TEST_BEGIN(allowlist_blocks_unlisted_apis)
+ {
+ switch_stream_handle_t stream = { 0 };
+
+ /* allowed: mod_commands.status.api */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("status", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
+ switch_safe_free(stream.data);
+
+ /* allowed: mod_commands.version */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("version", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
+ switch_safe_free(stream.data);
+
+ /* blocked: the shell-exec API this whole feature exists to disable */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("system", "", NULL, &stream) == SWITCH_STATUS_FALSE);
+ switch_safe_free(stream.data);
+
+ /* blocked: spawn family */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("spawn", "", NULL, &stream) == SWITCH_STATUS_FALSE);
+ switch_safe_free(stream.data);
+
+ /* blocked: an ordinary unlisted command, proving default-deny once the list is active */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("uptime", "", NULL, &stream) == SWITCH_STATUS_FALSE);
+ switch_safe_free(stream.data);
+ }
+ FST_TEST_END()
+
+ /* "mod_commands.status.api" permits only the API type; the JSON API of the same name stays blocked. */
+ FST_TEST_BEGIN(allowlist_type_qualified_entry)
+ {
+ switch_stream_handle_t stream = { 0 };
+ cJSON *jcmd, *reply = NULL;
+ switch_status_t status;
+
+ /* API "status" is permitted. */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("status", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
+ switch_safe_free(stream.data);
+
+ /* JSON API "status" is NOT permitted (only the .api type was allowed) -> not found. */
+ jcmd = cJSON_Parse("{\"command\":\"status\",\"data\":\"\"}");
+ fst_requires(jcmd);
+ status = switch_json_api_execute(jcmd, NULL, &reply);
+ fst_check(status == SWITCH_STATUS_FALSE);
+ if (reply) {
+ cJSON_Delete(reply);
+ }
+ cJSON_Delete(jcmd);
+ }
+ FST_TEST_END()
+
+ /* interface_allowlist_dump emits keys in the config format. It reflects module capabilities
+ (every interface the module registered), independent of what enforcement blocked -- that is
+ what makes it useful for generating a starting allowlist. */
+ FST_TEST_BEGIN(interface_allowlist_dump_format)
+ {
+ switch_stream_handle_t stream = { 0 };
+
+ /* Default: XML, one fully qualified entry per interface. */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("interface_allowlist_dump", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
+ fst_requires(stream.data);
+ fst_check(strstr((char *) stream.data, "<interface-allowlist>") != NULL);
+ fst_check(strstr((char *) stream.data, "mod_commands.status.api") != NULL);
+ fst_check(strstr((char *) stream.data, "mod_commands.status.json_api") != NULL);
+ /* system is present in the dump even though it was blocked from registering. */
+ fst_check(strstr((char *) stream.data, "mod_commands.system.api") != NULL);
+ switch_safe_free(stream.data);
+
+ /* "modules" variant: one entry per module. */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("interface_allowlist_dump", "modules", NULL, &stream) == SWITCH_STATUS_SUCCESS);
+ fst_requires(stream.data);
+ fst_check(strstr((char *) stream.data, "<allow name=\"mod_commands\"/>") != NULL);
+ switch_safe_free(stream.data);
+
+ /* "plain" variant: raw keys, no XML wrapper. */
+ SWITCH_STANDARD_STREAM(stream);
+ fst_check(switch_api_execute("interface_allowlist_dump", "plain", NULL, &stream) == SWITCH_STATUS_SUCCESS);
+ fst_requires(stream.data);
+ fst_check(strstr((char *) stream.data, "<interface-allowlist>") == NULL);
+ fst_check(strstr((char *) stream.data, "mod_commands.version.api") != NULL);
+ switch_safe_free(stream.data);
+ }
+ FST_TEST_END()
+
+ FST_TEARDOWN_BEGIN()
+ {
+ }
+ FST_TEARDOWN_END()
+ }
+ FST_MODULE_END()
+}
+FST_CORE_END()
diff --git a/src/switch_loadable_module.c b/src/switch_loadable_module.c
index bacd2f012a..ff95f9bf3d 100644
--- a/src/switch_loadable_module.c
+++ b/src/switch_loadable_module.c
@@ -98,6 +98,8 @@ struct switch_loadable_module_container {
switch_hash_t *limit_hash;
switch_hash_t *database_hash;
switch_hash_t *secondary_recover_hash;
+ switch_hash_t *interface_allowlist;
+ switch_bool_t interface_allowlist_enabled;
switch_mutex_t *mutex;
switch_thread_rwlock_t *chat_rwlock;
switch_memory_pool_t *pool;
@@ -153,6 +155,57 @@ static void switch_loadable_module_runtime(void)
switch_mutex_unlock(loadable_modules.mutex);
}
+/*
+ When an interface allowlist is configured in switch.conf.xml, only interfaces that are
+ explicitly permitted are allowed to register. An <allow> entry may be written at three
+ levels of precision (an interface is permitted if it matches ANY configured entry):
+
+ "mod_commands" - every interface in the module
+ "mod_commands.system" - any interface named "system" (type omitted matches all)
+ "mod_commands.system.api" - only the given interface TYPE named "system"
+
+ The optional trailing type token is one of: "app" (dialplan application), "api",
+ "json_api", "chat_app" (chat application). This lets you permit, say, the APP named
+ "system" while still blocking an API or JSON API of the same name. With no allowlist
+ configured, everything is permitted (nothing is enforced).
+*/
+static switch_bool_t switch_loadable_module_interface_allowed(const char *modname, const char *name, const char *type)
+{
+ char full[512];
+
+ /* No allowlist configured: enforce nothing. */
+ if (!loadable_modules.interface_allowlist_enabled) {
+ return SWITCH_TRUE;
+ }
+
+ if (zstr(modname)) {
+ return SWITCH_TRUE;
+ }
+
+ /* Whole module permitted, e.g. "mod_commands". */
+ if (switch_core_hash_find(loadable_modules.interface_allowlist, modname)) {
+ return SWITCH_TRUE;
+ }
+
+ if (!zstr(name)) {
+ /* Any type of this name permitted, e.g. "mod_commands.system". */
+ switch_snprintf(full, sizeof(full), "%s.%s", modname, name);
+ if (switch_core_hash_find(loadable_modules.interface_allowlist, full)) {
+ return SWITCH_TRUE;
+ }
+
+ /* Only this specific type of this name permitted, e.g. "mod_commands.system.api". */
+ if (!zstr(type)) {
+ switch_snprintf(full, sizeof(full), "%s.%s.%s", modname, name, type);
+ if (switch_core_hash_find(loadable_modules.interface_allowlist, full)) {
+ return SWITCH_TRUE;
+ }
+ }
+ }
+
+ return SWITCH_FALSE;
+}
+
static switch_status_t switch_loadable_module_process(char *key, switch_loadable_module_t *new_module, switch_hash_t *event_hash)
{
switch_event_t *event;
@@ -325,6 +378,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
for (ptr = new_module->module_interface->application_interface; ptr; ptr = ptr->next) {
if (!ptr->interface_name) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load application interface from %s due to no interface name.\n", key);
+ } else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "app")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping Application '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding Application '%s'\n", ptr->interface_name);
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
@@ -355,6 +410,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
for (ptr = new_module->module_interface->chat_application_interface; ptr; ptr = ptr->next) {
if (!ptr->interface_name) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load application interface from %s due to no interface name.\n", key);
+ } else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "chat_app")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping Chat Application '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding Chat Application '%s'\n", ptr->interface_name);
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
@@ -385,6 +442,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
for (ptr = new_module->module_interface->api_interface; ptr; ptr = ptr->next) {
if (!ptr->interface_name) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load api interface from %s due to no interface name.\n", key);
+ } else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "api")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping API '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding API Function '%s'\n", ptr->interface_name);
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
@@ -415,6 +474,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
for (ptr = new_module->module_interface->json_api_interface; ptr; ptr = ptr->next) {
if (!ptr->interface_name) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load JSON api interface from %s due to no interface name.\n", key);
+ } else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "json_api")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping JSON API '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding JSON API Function '%s'\n", ptr->interface_name);
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
@@ -1984,6 +2045,91 @@ SWITCH_DECLARE(switch_status_t) switch_loadable_module_enumerate_loaded(switch_m
return SWITCH_STATUS_SUCCESS;
}
+static void dump_allowlist_entry(switch_stream_handle_t *stream, switch_bool_t xml, const char *modname, const char *name, const char *type)
+{
+ if (xml) {
+ stream->write_function(stream, " <allow name=\"%s.%s.%s\"/>\n", modname, name, type);
+ } else {
+ stream->write_function(stream, "%s.%s.%s\n", modname, name, type);
+ }
+}
+
+/*
+ Dump every loaded module's application / api / json_api / chat-application interfaces in the
+ interface-allowlist key format, so the current state can be captured and pruned offline into a
+ switch.conf.xml <interface-allowlist> section. Keys use module->key (the same name enforcement
+ matches on). by_module emits one entry per module ("mod_commands"); otherwise one fully
+ qualified entry per interface ("mod_commands.system.api"). xml wraps entries as <allow/> tags.
+*/
+SWITCH_DECLARE(void) switch_loadable_module_dump_interface_allowlist(switch_stream_handle_t *stream, switch_bool_t by_module, switch_bool_t xml)
+{
+ switch_hash_index_t *hi;
+ void *val;
+ switch_loadable_module_t *module;
+
+ if (xml) {
+ stream->write_function(stream, "<!-- generated by 'interface_allowlist_dump'; prune entries you want to block, then paste into switch.conf.xml -->\n");
+ stream->write_function(stream, "<interface-allowlist>\n");
+ }
+
+ switch_mutex_lock(loadable_modules.mutex);
+ for (hi = switch_core_hash_first(loadable_modules.module_hash); hi; hi = switch_core_hash_next(&hi)) {
+ switch_core_hash_this(hi, NULL, NULL, &val);
+ module = (switch_loadable_module_t *) val;
+
+ if (!module || zstr(module->key)) {
+ continue;
+ }
+
+ if (by_module) {
+ if (xml) {
+ stream->write_function(stream, " <allow name=\"%s\"/>\n", module->key);
+ } else {
+ stream->write_function(stream, "%s\n", module->key);
+ }
+ continue;
+ }
+
+ if (!module->module_interface) {
+ continue;
+ }
+
+ if (xml) {
+ stream->write_function(stream, " <!-- %s -->\n", module->key);
+ }
+
+ {
+ const switch_application_interface_t *ptr;
+ for (ptr = module->module_interface->application_interface; ptr; ptr = ptr->next) {
+ if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "app");
+ }
+ }
+ {
+ const switch_api_interface_t *ptr;
+ for (ptr = module->module_interface->api_interface; ptr; ptr = ptr->next) {
+ if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "api");
+ }
+ }
+ {
+ const switch_json_api_interface_t *ptr;
+ for (ptr = module->module_interface->json_api_interface; ptr; ptr = ptr->next) {
+ if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "json_api");
+ }
+ }
+ {
+ const switch_chat_application_interface_t *ptr;
+ for (ptr = module->module_interface->chat_application_interface; ptr; ptr = ptr->next) {
+ if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "chat_app");
+ }
+ }
+ }
+ switch_mutex_unlock(loadable_modules.mutex);
+
+ if (xml) {
+ stream->write_function(stream, "</interface-allowlist>\n");
+ }
+}
+
SWITCH_DECLARE(switch_status_t) switch_loadable_module_build_dynamic(char *filename,
switch_module_load_t switch_module_load,
switch_module_runtime_t switch_module_runtime,
@@ -2136,7 +2282,46 @@ SWITCH_DECLARE(switch_status_t) switch_loadable_module_init(switch_bool_t autolo
switch_core_hash_init(&loadable_modules.secondary_recover_hash);
switch_mutex_init(&loadable_modules.mutex, SWITCH_MUTEX_NESTED, loadable_modules.pool);
switch_thread_rwlock_create(&loadable_modules.chat_rwlock, loadable_modules.pool);
-
+
+ /*
+ Build the optional interface allowlist from switch.conf.xml. When at least one <allow>
+ entry is present, ONLY the permitted modules (e.g. "mod_commands") or specific interfaces
+ (e.g. "mod_commands.system") may register application/api/json_api/chat-application
+ interfaces; every other such interface is refused at load time. With no allowlist
+ configured, nothing is enforced (all interfaces load as before). Built before the
+ autoload check so runtime module loads are subject to the same policy.
+ */
+ switch_core_hash_init_nocase(&loadable_modules.interface_allowlist);
+ loadable_modules.interface_allowlist_enabled = SWITCH_FALSE;
+ {
+ switch_xml_t cfg_al = NULL, xml_al = NULL, list = NULL, item = NULL;
+ if ((xml_al = switch_xml_open_cfg("switch.conf", &cfg_al, NULL))) {
+ if ((list = switch_xml_child(cfg_al, "interface-allowlist"))) {
+ for (item = switch_xml_child(list, "allow"); item; item = item->next) {
+ const char *aname = switch_xml_attr_soft(item, "name");
+ if (!zstr(aname)) {
+ char *entry = switch_core_strdup(loadable_modules.pool, aname);
+ switch_core_hash_insert(loadable_modules.interface_allowlist, entry, entry);
+ loadable_modules.interface_allowlist_enabled = SWITCH_TRUE;
+ }
+ }
+ if (!loadable_modules.interface_allowlist_enabled) {
+ /* Section present but nothing usable parsed (e.g. a typo'd tag or missing name= attribute).
+ Enforcement is boot-only, so a silent miss would leave the box unhardened with no runtime
+ safety net; make the misconfiguration loud instead of failing open quietly. */
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR,
+ "Interface allowlist section <interface-allowlist> is present but no valid <allow name=\"...\"/> "
+ "entries were found; enforcement is DISABLED. Check for typos (lowercase <allow> with a name= attribute).\n");
+ }
+ }
+ switch_xml_free(xml_al);
+ }
+ if (loadable_modules.interface_allowlist_enabled) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
+ "Interface allowlist ACTIVE: only explicitly permitted application/api/json_api interfaces will load.\n");
+ }
+ }
+
if (!autoload) return SWITCH_STATUS_SUCCESS;
/*
@@ -2472,6 +2657,7 @@ SWITCH_DECLARE(void) switch_loadable_module_shutdown(void)
switch_core_hash_destroy(&loadable_modules.database_hash);
switch_core_hash_destroy(&loadable_modules.dialplan_hash);
switch_core_hash_destroy(&loadable_modules.secondary_recover_hash);
+ switch_core_hash_destroy(&loadable_modules.interface_allowlist);
switch_core_destroy_memory_pool(&loadable_modules.pool);
}