From 28f251f634652c7b6ca375229ad241a944cf4b38 Mon Sep 17 00:00:00 2001 From: Chris Rienzo Date: Mon, 13 Jul 2026 10:38:26 -0500 Subject: [PATCH] [Core, mod_commands] Interface allowlist * [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 configured nothing is enforced; when at least one 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 (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 + + 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); }