[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 <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
This commit is contained in:
Chris Rienzo
2026-07-13 10:38:26 -05:00
committed by Andrey Volk
parent 0a54a48f37
commit 28f251f634
8 changed files with 454 additions and 1 deletions
+1
View File
@@ -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
@@ -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>
+8
View File
@@ -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
@@ -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)
@@ -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, "");
@@ -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>
@@ -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()
+187 -1
View File
@@ -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);
}