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 @@
+
+
+
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 tags inside an 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, " ");
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+ *
+ * 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
+ *
+ * test_interface_allowlist -- tests the switch.conf.xml
+ *
+ * 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
+#include
+
+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, "") != 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, "") != 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, "") == 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 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, " \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 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 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, "\n");
+ stream->write_function(stream, "\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, " \n", module->key);
+ } else {
+ stream->write_function(stream, "%s\n", module->key);
+ }
+ continue;
+ }
+
+ if (!module->module_interface) {
+ continue;
+ }
+
+ if (xml) {
+ stream->write_function(stream, " \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, "\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
+ 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 is present but no valid "
+ "entries were found; enforcement is DISABLED. Check for typos (lowercase 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);
}